Merge branch 'master' into codex/upstream-pr
This commit is contained in:
@@ -39,9 +39,9 @@
|
||||
return '';
|
||||
}
|
||||
|
||||
function extractFailedStep(status = '', detail = '') {
|
||||
function extractRecordStep(status = '', detail = '') {
|
||||
const normalizedStatus = String(status || '').trim().toLowerCase();
|
||||
const statusMatch = normalizedStatus.match(/^step(\d+)_failed$/);
|
||||
const statusMatch = normalizedStatus.match(/^step(\d+)_(?:failed|stopped)$/);
|
||||
if (statusMatch) {
|
||||
const step = Number(statusMatch[1]);
|
||||
return Number.isInteger(step) && step > 0 ? step : null;
|
||||
@@ -73,6 +73,9 @@
|
||||
return '流程完成';
|
||||
}
|
||||
if (finalStatus === 'stopped') {
|
||||
if (Number.isInteger(failedStep) && failedStep > 0) {
|
||||
return `步骤 ${failedStep} 停止`;
|
||||
}
|
||||
return '流程已停止';
|
||||
}
|
||||
if (finalStatus !== 'failed') {
|
||||
@@ -152,7 +155,7 @@
|
||||
const failedStepCandidate = Number(record.failedStep);
|
||||
const failedStep = Number.isInteger(failedStepCandidate) && failedStepCandidate > 0
|
||||
? failedStepCandidate
|
||||
: extractFailedStep(record.finalStatus || record.status || '', failureDetail);
|
||||
: extractRecordStep(record.finalStatus || record.status || '', failureDetail);
|
||||
const autoRunContext = normalizeAutoRunContext(record.autoRunContext);
|
||||
const retryCount = normalizeRetryCount(
|
||||
record.retryCount !== undefined
|
||||
@@ -160,6 +163,8 @@
|
||||
: ((autoRunContext?.attemptRun || 0) > 1 ? autoRunContext.attemptRun - 1 : 0)
|
||||
);
|
||||
const source = normalizeSource(record.source || (autoRunContext ? 'auto' : 'manual'));
|
||||
const computedFailureLabel = buildFailureLabel(finalStatus, failedStep, failureDetail);
|
||||
const rawFailureLabel = String(record.failureLabel || '').trim();
|
||||
|
||||
return {
|
||||
recordId: String(record.recordId || '').trim() || buildRecordId(email),
|
||||
@@ -168,7 +173,9 @@
|
||||
finalStatus,
|
||||
finishedAt,
|
||||
retryCount,
|
||||
failureLabel: String(record.failureLabel || '').trim() || buildFailureLabel(finalStatus, failedStep, failureDetail),
|
||||
failureLabel: finalStatus === 'stopped'
|
||||
? computedFailureLabel
|
||||
: (rawFailureLabel || computedFailureLabel),
|
||||
failureDetail,
|
||||
failedStep: Number.isInteger(failedStep) && failedStep > 0 ? failedStep : null,
|
||||
source,
|
||||
@@ -215,7 +222,9 @@
|
||||
}
|
||||
|
||||
const failureDetail = finalStatus === 'failed' || finalStatus === 'stopped' ? String(reason || '').trim() : '';
|
||||
const failedStep = finalStatus === 'failed' ? extractFailedStep(status, failureDetail) : null;
|
||||
const failedStep = finalStatus === 'failed' || finalStatus === 'stopped'
|
||||
? extractRecordStep(status, failureDetail)
|
||||
: null;
|
||||
const source = Boolean(state.autoRunning) ? 'auto' : 'manual';
|
||||
const autoRunContext = source === 'auto' ? buildAutoRunContextFromState(state) : null;
|
||||
const retryCount = source === 'auto' ? getRetryCountFromState(state) : 0;
|
||||
@@ -322,6 +331,9 @@
|
||||
}
|
||||
|
||||
function shouldSyncAccountRunHistorySnapshot(state = {}) {
|
||||
if (Boolean(state.contributionMode)) {
|
||||
return false;
|
||||
}
|
||||
if (!Boolean(state.accountRunHistoryTextEnabled)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
hasSavedProgress,
|
||||
isAddPhoneAuthFailure,
|
||||
isRestartCurrentAttemptError,
|
||||
isSignupUserAlreadyExistsFailure,
|
||||
isStopError,
|
||||
launchAutoRunTimerPlan,
|
||||
normalizeAutoRunFallbackThreadIntervalMinutes,
|
||||
@@ -458,7 +459,9 @@
|
||||
const reason = getErrorMessage(err);
|
||||
roundSummary.failureReasons.push(reason);
|
||||
const blockedByAddPhone = typeof isAddPhoneAuthFailure === 'function' && isAddPhoneAuthFailure(err);
|
||||
const canRetry = !blockedByAddPhone && autoRunSkipFailures && attemptRun < maxAttemptsForRound;
|
||||
const blockedBySignupUserAlreadyExists = typeof isSignupUserAlreadyExistsFailure === 'function'
|
||||
&& isSignupUserAlreadyExistsFailure(err);
|
||||
const canRetry = !blockedByAddPhone && !blockedBySignupUserAlreadyExists && autoRunSkipFailures && attemptRun < maxAttemptsForRound;
|
||||
|
||||
await setState({
|
||||
autoRunRoundSummaries: serializeAutoRunRoundSummaries(totalRuns, roundSummaries),
|
||||
@@ -499,6 +502,41 @@
|
||||
break;
|
||||
}
|
||||
|
||||
if (blockedBySignupUserAlreadyExists) {
|
||||
roundSummary.status = 'failed';
|
||||
roundSummary.finalFailureReason = reason;
|
||||
await setState({
|
||||
autoRunRoundSummaries: serializeAutoRunRoundSummaries(totalRuns, roundSummaries),
|
||||
});
|
||||
await appendRoundRecordIfNeeded('failed', reason);
|
||||
cancelPendingCommands('当前轮因 user_already_exists 已终止。');
|
||||
await broadcastStopToContentScripts();
|
||||
if (!autoRunSkipFailures) {
|
||||
await addLog(
|
||||
`第 ${targetRun}/${totalRuns} 轮触发 user_already_exists/用户已存在,自动重试未开启,当前自动运行将停止。`,
|
||||
'warn'
|
||||
);
|
||||
stoppedEarly = true;
|
||||
await broadcastAutoRunStatus('stopped', {
|
||||
currentRun: targetRun,
|
||||
totalRuns,
|
||||
attemptRun,
|
||||
sessionId: 0,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
await addLog(`第 ${targetRun}/${totalRuns} 轮触发 user_already_exists/用户已存在,本轮将直接失败并跳过剩余重试。`, 'warn');
|
||||
await addLog(
|
||||
targetRun < totalRuns
|
||||
? `第 ${targetRun}/${totalRuns} 轮因 user_already_exists/用户已存在提前结束,自动流程将继续下一轮。`
|
||||
: `第 ${targetRun}/${totalRuns} 轮因 user_already_exists/用户已存在提前结束,已无后续轮次,本次自动运行结束。`,
|
||||
'warn'
|
||||
);
|
||||
forceFreshTabsNextRun = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (canRetry) {
|
||||
const retryIndex = attemptRun;
|
||||
if (isRestartCurrentAttemptError(err)) {
|
||||
|
||||
@@ -0,0 +1,689 @@
|
||||
(function attachBackgroundContributionOAuth(root, factory) {
|
||||
root.MultiPageBackgroundContributionOAuth = factory();
|
||||
})(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', 'manual_review_required', 'expired', 'error']);
|
||||
const CALLBACK_FINAL_STATUSES = new Set(['submitted']);
|
||||
const CALLBACK_WAITING_STATUSES = new Set(['idle', 'waiting', 'captured', 'failed', 'submitting']);
|
||||
|
||||
const RUNTIME_DEFAULTS = {
|
||||
contributionMode: false,
|
||||
contributionModeExpected: false,
|
||||
contributionNickname: '',
|
||||
contributionQq: '',
|
||||
contributionSessionId: '',
|
||||
contributionAuthUrl: '',
|
||||
contributionAuthState: '',
|
||||
contributionCallbackUrl: '',
|
||||
contributionStatus: '',
|
||||
contributionStatusMessage: '',
|
||||
contributionLastPollAt: 0,
|
||||
contributionCallbackStatus: 'idle',
|
||||
contributionCallbackMessage: '',
|
||||
contributionAuthOpenedAt: 0,
|
||||
contributionAuthTabId: 0,
|
||||
};
|
||||
|
||||
const RUNTIME_KEYS = Object.keys(RUNTIME_DEFAULTS);
|
||||
|
||||
function createContributionOAuthManager(deps = {}) {
|
||||
const {
|
||||
addLog,
|
||||
broadcastDataUpdate,
|
||||
chrome,
|
||||
closeLocalhostCallbackTabs,
|
||||
getState,
|
||||
setState,
|
||||
} = deps;
|
||||
|
||||
let listenersBound = false;
|
||||
|
||||
function normalizeString(value = '') {
|
||||
return String(value || '').trim();
|
||||
}
|
||||
|
||||
function normalizePositiveInteger(value, fallback = 0) {
|
||||
const numeric = Math.floor(Number(value) || 0);
|
||||
return numeric > 0 ? numeric : fallback;
|
||||
}
|
||||
|
||||
function normalizeContributionStatus(value = '') {
|
||||
const normalized = normalizeString(value).toLowerCase();
|
||||
switch (normalized) {
|
||||
case 'started':
|
||||
return 'started';
|
||||
case 'waiting':
|
||||
case 'wait':
|
||||
return 'waiting';
|
||||
case 'processing':
|
||||
return 'processing';
|
||||
case 'auto_approved':
|
||||
case 'approved':
|
||||
return 'auto_approved';
|
||||
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';
|
||||
case 'error':
|
||||
case 'failed':
|
||||
return 'error';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeContributionCallbackStatus(value = '') {
|
||||
const normalized = normalizeString(value).toLowerCase();
|
||||
switch (normalized) {
|
||||
case 'idle':
|
||||
return 'idle';
|
||||
case 'waiting':
|
||||
case 'pending':
|
||||
return 'waiting';
|
||||
case 'captured':
|
||||
return 'captured';
|
||||
case 'submitting':
|
||||
case 'processing':
|
||||
return 'submitting';
|
||||
case 'submitted':
|
||||
case 'success':
|
||||
case 'done':
|
||||
return 'submitted';
|
||||
case 'failed':
|
||||
case 'error':
|
||||
return 'failed';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function isContributionFinalStatus(status = '') {
|
||||
return FINAL_STATUSES.has(normalizeContributionStatus(status));
|
||||
}
|
||||
|
||||
function getStatusLabel(status = '') {
|
||||
switch (normalizeContributionStatus(status)) {
|
||||
case 'started':
|
||||
return '已生成登录地址';
|
||||
case 'waiting':
|
||||
return '等待提交回调';
|
||||
case 'processing':
|
||||
return '已提交回调,等待 CPA 确认';
|
||||
case 'auto_approved':
|
||||
return '贡献成功,CPA 已确认';
|
||||
case 'auto_rejected':
|
||||
return '贡献未通过确认';
|
||||
case 'manual_review_required':
|
||||
return '已提交,等待人工处理';
|
||||
case 'expired':
|
||||
return '贡献会话已超时';
|
||||
case 'error':
|
||||
return '贡献流程失败';
|
||||
default:
|
||||
return '等待开始贡献';
|
||||
}
|
||||
}
|
||||
|
||||
function getCallbackLabel(status = '') {
|
||||
switch (normalizeContributionCallbackStatus(status)) {
|
||||
case 'waiting':
|
||||
case 'idle':
|
||||
return '等待回调';
|
||||
case 'captured':
|
||||
return '已捕获回调地址';
|
||||
case 'submitting':
|
||||
return '正在提交回调';
|
||||
case 'submitted':
|
||||
return '已提交回调';
|
||||
case 'failed':
|
||||
return '回调提交失败';
|
||||
default:
|
||||
return '等待回调';
|
||||
}
|
||||
}
|
||||
|
||||
function unwrapPayload(payload) {
|
||||
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (payload.data && typeof payload.data === 'object' && !Array.isArray(payload.data)) {
|
||||
return { ...payload.data, ...payload };
|
||||
}
|
||||
|
||||
return payload;
|
||||
}
|
||||
|
||||
function getErrorMessage(payload, responseStatus = 500) {
|
||||
const details = [
|
||||
payload?.message,
|
||||
payload?.detail,
|
||||
payload?.error,
|
||||
payload?.reason,
|
||||
]
|
||||
.map((item) => normalizeString(item))
|
||||
.find(Boolean);
|
||||
|
||||
if (details) {
|
||||
return details;
|
||||
}
|
||||
|
||||
return `贡献服务请求失败(HTTP ${responseStatus})。`;
|
||||
}
|
||||
|
||||
async function fetchContributionJson(endpoint, options = {}) {
|
||||
const controller = new AbortController();
|
||||
const timeoutMs = Math.max(1000, Math.floor(Number(options.timeoutMs) || 15000));
|
||||
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}${endpoint}`, {
|
||||
method: options.method || 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
...(options.body ? { 'Content-Type': 'application/json' } : {}),
|
||||
},
|
||||
body: options.body ? JSON.stringify(options.body) : undefined,
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
let rawPayload = {};
|
||||
try {
|
||||
rawPayload = await response.json();
|
||||
} catch {
|
||||
rawPayload = {};
|
||||
}
|
||||
|
||||
const payload = unwrapPayload(rawPayload);
|
||||
if (!response.ok || payload.ok === false) {
|
||||
const error = new Error(getErrorMessage(payload, response.status));
|
||||
error.payload = payload;
|
||||
error.responseStatus = response.status;
|
||||
throw error;
|
||||
}
|
||||
|
||||
return payload;
|
||||
} catch (error) {
|
||||
if (error?.name === 'AbortError') {
|
||||
throw new Error('贡献服务请求超时,请稍后重试。');
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
|
||||
function pickContributionState(state = {}) {
|
||||
const picked = {};
|
||||
for (const key of RUNTIME_KEYS) {
|
||||
picked[key] = state[key] !== undefined ? state[key] : RUNTIME_DEFAULTS[key];
|
||||
}
|
||||
return picked;
|
||||
}
|
||||
|
||||
async function applyRuntimeUpdates(updates = {}) {
|
||||
if (!updates || typeof updates !== 'object' || Array.isArray(updates) || Object.keys(updates).length === 0) {
|
||||
return getState();
|
||||
}
|
||||
|
||||
await setState(updates);
|
||||
broadcastDataUpdate(updates);
|
||||
return getState();
|
||||
}
|
||||
|
||||
function extractAuthStateFromUrl(authUrl = '') {
|
||||
try {
|
||||
return new URL(authUrl).searchParams.get('state') || '';
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function buildNickname(state = {}, preferredNickname = '') {
|
||||
const nickname = normalizeString(preferredNickname)
|
||||
|| normalizeString(state.email)
|
||||
|| normalizeString(state.contributionNickname);
|
||||
return nickname || 'codex-extension-user';
|
||||
}
|
||||
|
||||
function buildContributionQq(state = {}, preferredQq = '') {
|
||||
const qq = normalizeString(preferredQq) || normalizeString(state.contributionQq);
|
||||
return qq;
|
||||
}
|
||||
|
||||
function buildStatusMessage(status, payload = {}) {
|
||||
const label = getStatusLabel(status);
|
||||
const details = [
|
||||
payload.status_message,
|
||||
payload.statusMessage,
|
||||
payload.message,
|
||||
payload.detail,
|
||||
payload.reason,
|
||||
]
|
||||
.map((item) => normalizeString(item))
|
||||
.find(Boolean);
|
||||
|
||||
if (!details || details === label) {
|
||||
return label;
|
||||
}
|
||||
|
||||
return `${label}:${details}`;
|
||||
}
|
||||
|
||||
function buildCallbackMessage(status, payload = {}) {
|
||||
const label = getCallbackLabel(status);
|
||||
const details = [
|
||||
payload.callback_message,
|
||||
payload.callbackMessage,
|
||||
payload.message,
|
||||
payload.detail,
|
||||
payload.reason,
|
||||
]
|
||||
.map((item) => normalizeString(item))
|
||||
.find(Boolean);
|
||||
|
||||
if (!details || details === label) {
|
||||
return label;
|
||||
}
|
||||
|
||||
return `${label}:${details}`;
|
||||
}
|
||||
|
||||
function deriveCallbackState(payload = {}, state = {}) {
|
||||
const existingStatus = normalizeContributionCallbackStatus(state.contributionCallbackStatus);
|
||||
const callbackUrl = normalizeString(
|
||||
payload.callback_url
|
||||
|| payload.callbackUrl
|
||||
|| state.contributionCallbackUrl
|
||||
);
|
||||
const explicitStatus = normalizeContributionCallbackStatus(
|
||||
payload.callback_status
|
||||
|| payload.callbackStatus
|
||||
);
|
||||
|
||||
if (explicitStatus) {
|
||||
return {
|
||||
status: explicitStatus,
|
||||
message: buildCallbackMessage(explicitStatus, payload),
|
||||
callbackUrl,
|
||||
};
|
||||
}
|
||||
|
||||
if (payload.callback_submitted === true || payload.callbackSubmitted === true) {
|
||||
return {
|
||||
status: 'submitted',
|
||||
message: buildCallbackMessage('submitted', payload),
|
||||
callbackUrl,
|
||||
};
|
||||
}
|
||||
|
||||
if (callbackUrl) {
|
||||
return {
|
||||
status: CALLBACK_FINAL_STATUSES.has(existingStatus) ? existingStatus : 'captured',
|
||||
message: buildCallbackMessage(CALLBACK_FINAL_STATUSES.has(existingStatus) ? existingStatus : 'captured', payload),
|
||||
callbackUrl,
|
||||
};
|
||||
}
|
||||
|
||||
if (CALLBACK_FINAL_STATUSES.has(existingStatus) || existingStatus === 'failed') {
|
||||
return {
|
||||
status: existingStatus,
|
||||
message: normalizeString(state.contributionCallbackMessage) || buildCallbackMessage(existingStatus),
|
||||
callbackUrl: normalizeString(state.contributionCallbackUrl),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: 'waiting',
|
||||
message: buildCallbackMessage('waiting', payload),
|
||||
callbackUrl: '',
|
||||
};
|
||||
}
|
||||
|
||||
function isContributionCallbackUrl(rawUrl, state = {}) {
|
||||
const urlText = normalizeString(rawUrl);
|
||||
if (!urlText) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let parsed;
|
||||
try {
|
||||
parsed = new URL(urlText);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!['http:', 'https:'].includes(parsed.protocol)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const code = normalizeString(parsed.searchParams.get('code'));
|
||||
const errorText = normalizeString(parsed.searchParams.get('error'))
|
||||
|| normalizeString(parsed.searchParams.get('error_description'));
|
||||
const authState = normalizeString(parsed.searchParams.get('state'));
|
||||
if ((!code && !errorText) || !authState) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const hostLooksLocal = ['localhost', '127.0.0.1'].includes(parsed.hostname);
|
||||
const pathLooksLikeCallback = /callback/i.test(parsed.pathname || '');
|
||||
if (!hostLooksLocal && !pathLooksLikeCallback) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const expectedState = normalizeString(state.contributionAuthState);
|
||||
return !expectedState || expectedState === authState;
|
||||
}
|
||||
|
||||
async function openContributionAuthUrl(authUrl, options = {}) {
|
||||
const normalizedUrl = normalizeString(authUrl);
|
||||
if (!normalizedUrl) {
|
||||
throw new Error('贡献服务未返回有效的登录地址。');
|
||||
}
|
||||
|
||||
const currentState = options.stateOverride || await getState();
|
||||
const preferredTabId = normalizePositiveInteger(options.tabId || currentState.contributionAuthTabId, 0);
|
||||
let tab = null;
|
||||
|
||||
if (preferredTabId) {
|
||||
tab = await chrome.tabs.update(preferredTabId, {
|
||||
url: normalizedUrl,
|
||||
active: true,
|
||||
}).catch(() => null);
|
||||
}
|
||||
|
||||
if (!tab) {
|
||||
tab = await chrome.tabs.create({ url: normalizedUrl, active: true });
|
||||
}
|
||||
|
||||
await applyRuntimeUpdates({
|
||||
contributionAuthUrl: normalizedUrl,
|
||||
contributionAuthOpenedAt: Date.now(),
|
||||
contributionAuthTabId: normalizePositiveInteger(tab?.id, 0),
|
||||
});
|
||||
|
||||
return tab;
|
||||
}
|
||||
|
||||
async function fetchContributionResult(sessionId) {
|
||||
try {
|
||||
return await fetchContributionJson(`/result?session_id=${encodeURIComponent(sessionId)}`);
|
||||
} catch (error) {
|
||||
if (typeof addLog === 'function') {
|
||||
await addLog(`贡献模式:获取最终结果失败:${error.message}`, 'warn');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function submitContributionCallback(callbackUrl, options = {}) {
|
||||
const currentState = options.stateOverride || await getState();
|
||||
const sessionId = normalizeString(currentState.contributionSessionId);
|
||||
const normalizedUrl = normalizeString(callbackUrl);
|
||||
|
||||
if (!sessionId || !normalizedUrl) {
|
||||
return currentState;
|
||||
}
|
||||
|
||||
const currentCallbackStatus = normalizeContributionCallbackStatus(currentState.contributionCallbackStatus);
|
||||
if (CALLBACK_FINAL_STATUSES.has(currentCallbackStatus) || currentCallbackStatus === 'submitting') {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
const currentCallbackStatus = normalizeContributionCallbackStatus(currentState.contributionCallbackStatus);
|
||||
if (
|
||||
normalizedUrl
|
||||
&& normalizeString(currentState.contributionCallbackUrl) === normalizedUrl
|
||||
&& (CALLBACK_FINAL_STATUSES.has(currentCallbackStatus) || currentCallbackStatus === 'submitting')
|
||||
) {
|
||||
return currentState;
|
||||
}
|
||||
|
||||
await applyRuntimeUpdates({
|
||||
contributionCallbackUrl: normalizedUrl,
|
||||
contributionCallbackStatus: 'captured',
|
||||
contributionCallbackMessage: buildCallbackMessage('captured'),
|
||||
});
|
||||
|
||||
if (typeof addLog === 'function') {
|
||||
await addLog(`贡献模式:已捕获回调地址(${metadata.source || 'unknown'})。`, 'info');
|
||||
}
|
||||
|
||||
try {
|
||||
return await submitContributionCallback(normalizedUrl, {
|
||||
reason: metadata.source || 'navigation',
|
||||
stateOverride: await getState(),
|
||||
});
|
||||
} catch {
|
||||
return getState();
|
||||
}
|
||||
}
|
||||
|
||||
async function pollContributionStatus(options = {}) {
|
||||
const currentState = options.stateOverride || await getState();
|
||||
const sessionId = normalizeString(currentState.contributionSessionId);
|
||||
if (!sessionId) {
|
||||
return currentState;
|
||||
}
|
||||
|
||||
const payload = await fetchContributionJson(`/status?session_id=${encodeURIComponent(sessionId)}`);
|
||||
const nextStatus = normalizeContributionStatus(payload.status || payload.state || payload.phase) || currentState.contributionStatus || 'waiting';
|
||||
let finalPayload = null;
|
||||
|
||||
if (isContributionFinalStatus(nextStatus)) {
|
||||
finalPayload = await fetchContributionResult(sessionId);
|
||||
}
|
||||
|
||||
const mergedPayload = finalPayload ? { ...payload, ...finalPayload } : payload;
|
||||
const normalizedStatus = normalizeContributionStatus(mergedPayload.status || mergedPayload.state || mergedPayload.phase) || nextStatus;
|
||||
const callbackState = deriveCallbackState(mergedPayload, currentState);
|
||||
const updates = {
|
||||
contributionLastPollAt: Date.now(),
|
||||
contributionStatus: normalizedStatus,
|
||||
contributionStatusMessage: buildStatusMessage(normalizedStatus, mergedPayload),
|
||||
contributionCallbackUrl: callbackState.callbackUrl,
|
||||
contributionCallbackStatus: callbackState.status,
|
||||
contributionCallbackMessage: callbackState.message,
|
||||
};
|
||||
|
||||
const authUrl = normalizeString(mergedPayload.auth_url || mergedPayload.authUrl);
|
||||
if (authUrl) {
|
||||
updates.contributionAuthUrl = authUrl;
|
||||
}
|
||||
|
||||
const authState = normalizeString(mergedPayload.state || mergedPayload.auth_state || mergedPayload.authState)
|
||||
|| (authUrl ? extractAuthStateFromUrl(authUrl) : '');
|
||||
if (authState) {
|
||||
updates.contributionAuthState = authState;
|
||||
}
|
||||
|
||||
await applyRuntimeUpdates(updates);
|
||||
const nextState = await getState();
|
||||
|
||||
if (
|
||||
normalizeString(nextState.contributionCallbackUrl)
|
||||
&& CALLBACK_WAITING_STATUSES.has(normalizeContributionCallbackStatus(nextState.contributionCallbackStatus))
|
||||
) {
|
||||
try {
|
||||
return await submitContributionCallback(nextState.contributionCallbackUrl, {
|
||||
reason: options.reason || 'status_poll',
|
||||
stateOverride: nextState,
|
||||
});
|
||||
} catch {
|
||||
return getState();
|
||||
}
|
||||
}
|
||||
|
||||
return nextState;
|
||||
}
|
||||
|
||||
async function startContributionFlow(options = {}) {
|
||||
const currentState = options.stateOverride || await getState();
|
||||
const shouldOpenAuthTab = options.openAuthTab !== false;
|
||||
if (!currentState.contributionMode) {
|
||||
throw new Error('请先进入贡献模式。');
|
||||
}
|
||||
|
||||
const currentSessionId = normalizeString(currentState.contributionSessionId);
|
||||
const currentStatus = normalizeContributionStatus(currentState.contributionStatus);
|
||||
if (currentSessionId && ACTIVE_STATUSES.has(currentStatus)) {
|
||||
if (normalizeString(currentState.contributionAuthUrl)) {
|
||||
if (shouldOpenAuthTab) {
|
||||
await openContributionAuthUrl(currentState.contributionAuthUrl, {
|
||||
stateOverride: currentState,
|
||||
}).catch(() => null);
|
||||
}
|
||||
}
|
||||
return pollContributionStatus({ reason: 'resume_existing' });
|
||||
}
|
||||
|
||||
const payload = await fetchContributionJson('/start', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
nickname: buildNickname(currentState, options.nickname),
|
||||
qq: buildContributionQq(currentState, options.qq),
|
||||
source: 'cpa',
|
||||
channel: 'codex-extension',
|
||||
},
|
||||
});
|
||||
|
||||
const sessionId = normalizeString(payload.session_id || payload.sessionId);
|
||||
const authUrl = normalizeString(payload.auth_url || payload.authUrl);
|
||||
const authState = normalizeString(payload.state || payload.auth_state || payload.authState) || extractAuthStateFromUrl(authUrl);
|
||||
if (!sessionId || !authUrl) {
|
||||
throw new Error('贡献服务未返回有效的 session_id 或 auth_url。');
|
||||
}
|
||||
|
||||
await applyRuntimeUpdates({
|
||||
contributionSessionId: sessionId,
|
||||
contributionAuthUrl: authUrl,
|
||||
contributionAuthState: authState,
|
||||
contributionCallbackUrl: '',
|
||||
contributionStatus: normalizeContributionStatus(payload.status) || 'started',
|
||||
contributionStatusMessage: buildStatusMessage(normalizeContributionStatus(payload.status) || 'started', payload),
|
||||
contributionLastPollAt: 0,
|
||||
contributionCallbackStatus: 'waiting',
|
||||
contributionCallbackMessage: buildCallbackMessage('waiting'),
|
||||
contributionAuthOpenedAt: 0,
|
||||
contributionAuthTabId: 0,
|
||||
});
|
||||
|
||||
if (shouldOpenAuthTab) {
|
||||
await openContributionAuthUrl(authUrl);
|
||||
}
|
||||
return pollContributionStatus({ reason: 'after_start' });
|
||||
}
|
||||
|
||||
function onNavigationEvent(details = {}, source) {
|
||||
if (details?.frameId !== undefined && Number(details.frameId) !== 0) {
|
||||
return;
|
||||
}
|
||||
handleCapturedCallback(details?.url || '', {
|
||||
source,
|
||||
tabId: normalizePositiveInteger(details?.tabId, 0),
|
||||
}).catch(() => {});
|
||||
}
|
||||
|
||||
function onTabUpdated(tabId, changeInfo, tab) {
|
||||
const candidateUrl = normalizeString(changeInfo?.url || tab?.url);
|
||||
if (!candidateUrl) {
|
||||
return;
|
||||
}
|
||||
handleCapturedCallback(candidateUrl, {
|
||||
source: 'tabs.onUpdated',
|
||||
tabId: normalizePositiveInteger(tabId, 0),
|
||||
}).catch(() => {});
|
||||
}
|
||||
|
||||
function ensureCallbackListeners() {
|
||||
if (listenersBound) {
|
||||
return;
|
||||
}
|
||||
|
||||
chrome.webNavigation.onCommitted.addListener((details) => {
|
||||
onNavigationEvent(details, 'webNavigation.onCommitted');
|
||||
});
|
||||
chrome.webNavigation.onHistoryStateUpdated.addListener((details) => {
|
||||
onNavigationEvent(details, 'webNavigation.onHistoryStateUpdated');
|
||||
});
|
||||
chrome.tabs.onUpdated.addListener(onTabUpdated);
|
||||
listenersBound = true;
|
||||
}
|
||||
|
||||
return {
|
||||
ensureCallbackListeners,
|
||||
handleCapturedCallback,
|
||||
isContributionCallbackUrl,
|
||||
isContributionFinalStatus,
|
||||
pollContributionStatus,
|
||||
startContributionFlow,
|
||||
submitContributionCallback,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
ACTIVE_STATUSES,
|
||||
FINAL_STATUSES,
|
||||
RUNTIME_DEFAULTS,
|
||||
RUNTIME_KEYS,
|
||||
createContributionOAuthManager,
|
||||
};
|
||||
});
|
||||
@@ -94,6 +94,11 @@
|
||||
return /当前邮箱已存在,需要重新开始新一轮/.test(message);
|
||||
}
|
||||
|
||||
function isSignupUserAlreadyExistsFailure(error) {
|
||||
const message = getErrorMessage(error);
|
||||
return /SIGNUP_USER_ALREADY_EXISTS::|user_already_exists/i.test(message);
|
||||
}
|
||||
|
||||
function isStep9RecoverableAuthError(error) {
|
||||
const message = String(typeof error === 'string' ? error : error?.message || '');
|
||||
return /STEP9_OAUTH_RETRY::/i.test(message)
|
||||
@@ -165,6 +170,7 @@
|
||||
hasSavedProgress,
|
||||
isLegacyStep9RecoverableAuthError,
|
||||
isRestartCurrentAttemptError,
|
||||
isSignupUserAlreadyExistsFailure,
|
||||
isStep9RecoverableAuthError,
|
||||
isStepDoneStatus,
|
||||
isVerificationMailPollingError,
|
||||
|
||||
@@ -57,6 +57,7 @@
|
||||
notifyStepComplete,
|
||||
notifyStepError,
|
||||
patchHotmailAccount,
|
||||
pollContributionStatus,
|
||||
registerTab,
|
||||
requestStop,
|
||||
handleCloudflareSecurityBlocked,
|
||||
@@ -65,6 +66,7 @@
|
||||
scheduleAutoRun,
|
||||
selectLuckmailPurchase,
|
||||
setCurrentHotmailAccount,
|
||||
setContributionMode,
|
||||
setEmailState,
|
||||
setEmailStateSilently,
|
||||
setIcloudAliasPreservedState,
|
||||
@@ -77,6 +79,7 @@
|
||||
setStepStatus,
|
||||
skipAutoRunCountdown,
|
||||
skipStep,
|
||||
startContributionFlow,
|
||||
startAutoRunLoop,
|
||||
syncHotmailAccounts,
|
||||
testHotmailAccountMailAccess,
|
||||
@@ -289,6 +292,70 @@
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
case 'SET_CONTRIBUTION_MODE': {
|
||||
const enabled = Boolean(message.payload?.enabled);
|
||||
const state = await ensureManualInteractionAllowed(enabled ? '进入贡献模式' : '退出贡献模式');
|
||||
if (Object.values(state.stepStatuses || {}).some((status) => status === 'running')) {
|
||||
throw new Error(enabled ? '当前有步骤正在执行,无法进入贡献模式。' : '当前有步骤正在执行,无法退出贡献模式。');
|
||||
}
|
||||
if (typeof setContributionMode !== 'function') {
|
||||
throw new Error('贡献模式切换能力未接入。');
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
state: await setContributionMode(enabled),
|
||||
};
|
||||
}
|
||||
|
||||
case 'START_CONTRIBUTION_FLOW': {
|
||||
const state = await ensureManualInteractionAllowed('开始贡献');
|
||||
if (Object.values(state.stepStatuses || {}).some((status) => status === 'running')) {
|
||||
throw new Error('当前有步骤正在执行,无法开始贡献流程。');
|
||||
}
|
||||
if (typeof startContributionFlow !== 'function') {
|
||||
throw new Error('贡献 OAuth 流程尚未接入。');
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
state: await startContributionFlow({
|
||||
nickname: message.payload?.nickname,
|
||||
qq: message.payload?.qq,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
case 'SET_CONTRIBUTION_PROFILE': {
|
||||
const state = await getState();
|
||||
if (!state?.contributionMode) {
|
||||
throw new Error('请先进入贡献模式。');
|
||||
}
|
||||
const nickname = String(message.payload?.nickname || '').trim();
|
||||
const qq = String(message.payload?.qq || '').trim();
|
||||
if (qq && !/^\d{1,20}$/.test(qq)) {
|
||||
throw new Error('QQ 只能填写数字,且长度不能超过 20 位。');
|
||||
}
|
||||
await setState({
|
||||
contributionNickname: nickname,
|
||||
contributionQq: qq,
|
||||
});
|
||||
return {
|
||||
ok: true,
|
||||
state: await getState(),
|
||||
};
|
||||
}
|
||||
|
||||
case 'POLL_CONTRIBUTION_STATUS': {
|
||||
if (typeof pollContributionStatus !== 'function') {
|
||||
throw new Error('贡献状态轮询能力尚未接入。');
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
state: await pollContributionStatus({
|
||||
reason: message.payload?.reason || 'sidepanel_poll',
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
case 'CLEAR_ACCOUNT_RUN_HISTORY': {
|
||||
const state = await getState();
|
||||
if (isAutoRunLockedState(state)) {
|
||||
@@ -340,6 +407,17 @@
|
||||
|
||||
case 'AUTO_RUN': {
|
||||
clearStopRequest();
|
||||
if (Boolean(message.payload?.contributionMode) && typeof setContributionMode === 'function') {
|
||||
await setContributionMode(true);
|
||||
if (typeof setState === 'function') {
|
||||
const contributionNickname = String(message.payload?.contributionNickname || '').trim();
|
||||
const contributionQq = String(message.payload?.contributionQq || '').trim();
|
||||
await setState({
|
||||
contributionNickname,
|
||||
contributionQq,
|
||||
});
|
||||
}
|
||||
}
|
||||
const state = await getState();
|
||||
if (getPendingAutoRunTimerPlan(state)) {
|
||||
throw new Error('已有自动运行倒计时计划,请先取消或立即开始。');
|
||||
@@ -354,6 +432,17 @@
|
||||
|
||||
case 'SCHEDULE_AUTO_RUN': {
|
||||
clearStopRequest();
|
||||
if (Boolean(message.payload?.contributionMode) && typeof setContributionMode === 'function') {
|
||||
await setContributionMode(true);
|
||||
if (typeof setState === 'function') {
|
||||
const contributionNickname = String(message.payload?.contributionNickname || '').trim();
|
||||
const contributionQq = String(message.payload?.contributionQq || '').trim();
|
||||
await setState({
|
||||
contributionNickname,
|
||||
contributionQq,
|
||||
});
|
||||
}
|
||||
}
|
||||
const totalRuns = normalizeRunCount(message.payload?.totalRuns || 1);
|
||||
return await scheduleAutoRun(totalRuns, {
|
||||
delayMinutes: message.payload?.delayMinutes,
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
source: 'background',
|
||||
payload: {
|
||||
vpsPassword: state.vpsPassword,
|
||||
logStep: 6,
|
||||
logStep: 7,
|
||||
},
|
||||
}, {
|
||||
timeoutMs: 30000,
|
||||
@@ -121,7 +121,7 @@
|
||||
sub2apiPassword: state.sub2apiPassword,
|
||||
sub2apiGroupName: groupName,
|
||||
sub2apiDefaultProxyName: state.sub2apiDefaultProxyName,
|
||||
logStep: 6,
|
||||
logStep: 7,
|
||||
},
|
||||
}, {
|
||||
responseTimeoutMs: SUB2API_STEP1_RESPONSE_TIMEOUT_MS,
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
CLOUDFLARE_TEMP_EMAIL_PROVIDER,
|
||||
confirmCustomVerificationStepBypass,
|
||||
ensureStep8VerificationPageReady,
|
||||
executeStep7,
|
||||
getOAuthFlowRemainingMs,
|
||||
getOAuthFlowStepTimeoutMs,
|
||||
getMailConfig,
|
||||
@@ -19,17 +18,16 @@
|
||||
isVerificationMailPollingError,
|
||||
LUCKMAIL_PROVIDER,
|
||||
resolveVerificationStep,
|
||||
rerunStep7ForStep8Recovery,
|
||||
reuseOrCreateTab,
|
||||
setState,
|
||||
setStepStatus,
|
||||
shouldUseCustomRegistrationEmail,
|
||||
sleepWithStop,
|
||||
STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS,
|
||||
STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS,
|
||||
throwIfStopped,
|
||||
} = deps;
|
||||
|
||||
async function getStep8ReadyTimeoutMs(actionLabel) {
|
||||
async function getStep8ReadyTimeoutMs(actionLabel, expectedOauthUrl = '') {
|
||||
if (typeof getOAuthFlowStepTimeoutMs !== 'function') {
|
||||
return 15000;
|
||||
}
|
||||
@@ -37,10 +35,11 @@
|
||||
return getOAuthFlowStepTimeoutMs(15000, {
|
||||
step: 8,
|
||||
actionLabel,
|
||||
oauthUrl: expectedOauthUrl,
|
||||
});
|
||||
}
|
||||
|
||||
function getStep8RemainingTimeResolver() {
|
||||
function getStep8RemainingTimeResolver(expectedOauthUrl = '') {
|
||||
if (typeof getOAuthFlowRemainingMs !== 'function') {
|
||||
return undefined;
|
||||
}
|
||||
@@ -48,6 +47,7 @@
|
||||
return async (details = {}) => getOAuthFlowRemainingMs({
|
||||
step: 8,
|
||||
actionLabel: details.actionLabel || '登录验证码流程',
|
||||
oauthUrl: expectedOauthUrl,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -60,6 +60,7 @@
|
||||
if (mail.error) throw new Error(mail.error);
|
||||
|
||||
const stepStartedAt = Date.now();
|
||||
const verificationSessionKey = `8:${stepStartedAt}`;
|
||||
const authTabId = await getTabId('signup-page');
|
||||
|
||||
if (authTabId) {
|
||||
@@ -73,7 +74,7 @@
|
||||
|
||||
throwIfStopped();
|
||||
const pageState = await ensureStep8VerificationPageReady({
|
||||
timeoutMs: await getStep8ReadyTimeoutMs('确认登录验证码页已就绪'),
|
||||
timeoutMs: await getStep8ReadyTimeoutMs('确认登录验证码页已就绪', state?.oauthUrl || ''),
|
||||
});
|
||||
const shouldCompareVerificationEmail = mail.provider !== '2925';
|
||||
const displayedVerificationEmail = shouldCompareVerificationEmail
|
||||
@@ -130,8 +131,10 @@
|
||||
...state,
|
||||
step8VerificationTargetEmail: displayedVerificationEmail || '',
|
||||
}, mail, {
|
||||
filterAfterTimestamp: stepStartedAt,
|
||||
getRemainingTimeMs: getStep8RemainingTimeResolver(),
|
||||
filterAfterTimestamp: mail.provider === '2925' ? 0 : stepStartedAt,
|
||||
sessionKey: verificationSessionKey,
|
||||
disableTimeBudgetCap: mail.provider === '2925',
|
||||
getRemainingTimeMs: getStep8RemainingTimeResolver(state?.oauthUrl || ''),
|
||||
requestFreshCodeFirst: false,
|
||||
targetEmail: fixedTargetEmail,
|
||||
resendIntervalMs: (mail.provider === HOTMAIL_PROVIDER || mail.provider === '2925')
|
||||
@@ -140,19 +143,6 @@
|
||||
});
|
||||
}
|
||||
|
||||
async function rerunStep7ForStep8Recovery(options = {}) {
|
||||
const {
|
||||
logMessage = '步骤 8:正在回到步骤 7,重新发起登录验证码流程...',
|
||||
postStepDelayMs = 3000,
|
||||
} = options;
|
||||
const currentState = await getState();
|
||||
await addLog(logMessage, 'warn');
|
||||
await executeStep7(currentState);
|
||||
if (postStepDelayMs > 0) {
|
||||
await sleepWithStop(postStepDelayMs);
|
||||
}
|
||||
}
|
||||
|
||||
function isStep8RestartStep7Error(error) {
|
||||
const message = String(error?.message || error || '');
|
||||
return /STEP8_RESTART_STEP7::/i.test(message);
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
const mail = getMailConfig(state);
|
||||
if (mail.error) throw new Error(mail.error);
|
||||
const stepStartedAt = Date.now();
|
||||
const verificationSessionKey = `4:${stepStartedAt}`;
|
||||
const signupTabId = await getTabId('signup-page');
|
||||
if (!signupTabId) {
|
||||
throw new Error('认证页面标签页已关闭,无法继续步骤 4。');
|
||||
@@ -91,7 +92,9 @@
|
||||
}
|
||||
|
||||
await resolveVerificationStep(4, state, mail, {
|
||||
filterAfterTimestamp: stepStartedAt,
|
||||
filterAfterTimestamp: mail.provider === '2925' ? 0 : stepStartedAt,
|
||||
sessionKey: verificationSessionKey,
|
||||
disableTimeBudgetCap: mail.provider === '2925',
|
||||
requestFreshCodeFirst: mail.provider === HOTMAIL_PROVIDER ? false : true,
|
||||
resendIntervalMs: (mail.provider === HOTMAIL_PROVIDER || mail.provider === '2925')
|
||||
? 0
|
||||
|
||||
@@ -48,6 +48,7 @@
|
||||
? await getOAuthFlowStepTimeoutMs(180000, {
|
||||
step: 7,
|
||||
actionLabel: 'OAuth 登录并进入验证码页',
|
||||
oauthUrl,
|
||||
})
|
||||
: 180000;
|
||||
|
||||
|
||||
@@ -164,9 +164,10 @@
|
||||
const nextPayload = { ...payload };
|
||||
const intervalMs = Math.max(1, Number(nextPayload.intervalMs) || 3000);
|
||||
const baseMaxAttempts = Math.max(1, Number(nextPayload.maxAttempts) || 1);
|
||||
const disableTimeBudgetCap = Boolean(options.disableTimeBudgetCap);
|
||||
const remainingMs = await getRemainingTimeBudgetMs(step, options, actionLabel);
|
||||
|
||||
if (remainingMs !== null) {
|
||||
if (!disableTimeBudgetCap && remainingMs !== null) {
|
||||
nextPayload.maxAttempts = Math.max(
|
||||
1,
|
||||
Math.min(baseMaxAttempts, Math.floor(Math.max(0, remainingMs - 1000) / intervalMs) + 1)
|
||||
@@ -174,7 +175,7 @@
|
||||
}
|
||||
|
||||
const defaultResponseTimeoutMs = Math.max(45000, nextPayload.maxAttempts * intervalMs + 25000);
|
||||
const responseTimeoutMs = remainingMs === null
|
||||
const responseTimeoutMs = disableTimeBudgetCap || remainingMs === null
|
||||
? defaultResponseTimeoutMs
|
||||
: Math.max(1000, Math.min(defaultResponseTimeoutMs, remainingMs));
|
||||
|
||||
@@ -236,6 +237,58 @@
|
||||
return requestedAt;
|
||||
}
|
||||
|
||||
function shouldPreclear2925Mailbox(step, mail) {
|
||||
return mail?.provider === '2925' && (step === 4 || step === 8);
|
||||
}
|
||||
|
||||
async function clear2925MailboxBeforePolling(step, mail, options = {}) {
|
||||
if (!shouldPreclear2925Mailbox(step, mail)) {
|
||||
return;
|
||||
}
|
||||
|
||||
throwIfStopped();
|
||||
await addLog(`步骤 ${step}:开始刷新 2925 邮箱前先清空全部邮件,避免读取旧验证码邮件。`, 'warn');
|
||||
|
||||
try {
|
||||
const responseTimeoutMs = await getResponseTimeoutMsForStep(
|
||||
step,
|
||||
options,
|
||||
15000,
|
||||
'清空 2925 邮箱历史邮件'
|
||||
);
|
||||
const result = await sendToMailContentScriptResilient(
|
||||
mail,
|
||||
{
|
||||
type: 'DELETE_ALL_EMAILS',
|
||||
step,
|
||||
source: 'background',
|
||||
payload: {},
|
||||
},
|
||||
{
|
||||
timeoutMs: responseTimeoutMs,
|
||||
responseTimeoutMs,
|
||||
maxRecoveryAttempts: 2,
|
||||
}
|
||||
);
|
||||
|
||||
if (result?.error) {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
|
||||
if (result?.deleted === false) {
|
||||
await addLog(`步骤 ${step}:未能确认 2925 邮箱已清空,将继续刷新等待新邮件。`, 'warn');
|
||||
return;
|
||||
}
|
||||
|
||||
await addLog(`步骤 ${step}:2925 邮箱已预先清空,开始刷新等待新邮件。`, 'info');
|
||||
} catch (err) {
|
||||
if (isStopError(err)) {
|
||||
throw err;
|
||||
}
|
||||
await addLog(`步骤 ${step}:预清空 2925 邮箱失败,将继续刷新等待新邮件:${err.message}`, 'warn');
|
||||
}
|
||||
}
|
||||
|
||||
function triggerPostSuccessMailboxCleanup(step, mail) {
|
||||
if (mail?.provider !== '2925') {
|
||||
return;
|
||||
@@ -564,7 +617,7 @@
|
||||
getLegacyVerificationResendCountDefault(step, { requestFreshCodeFirst })
|
||||
)
|
||||
: getConfiguredVerificationResendCount(step, state, { requestFreshCodeFirst });
|
||||
const maxSubmitAttempts = 7;
|
||||
const maxSubmitAttempts = 15;
|
||||
const resendIntervalMs = Math.max(0, Number(options.resendIntervalMs) || 0);
|
||||
let lastResendAt = Number(options.lastResendAt) || 0;
|
||||
|
||||
@@ -572,6 +625,8 @@
|
||||
return nextFilterAfterTimestamp;
|
||||
};
|
||||
|
||||
await clear2925MailboxBeforePolling(step, mail, options);
|
||||
|
||||
if (requestFreshCodeFirst) {
|
||||
if (remainingAutomaticResendCount <= 0) {
|
||||
await addLog(`步骤 ${step}:当前自动重新发送验证码次数为 0,将直接使用当前时间窗口轮询邮箱。`, 'info');
|
||||
@@ -609,6 +664,7 @@
|
||||
for (let attempt = 1; attempt <= maxSubmitAttempts; attempt++) {
|
||||
const pollOptions = {
|
||||
excludeCodes: [...rejectedCodes],
|
||||
disableTimeBudgetCap: Boolean(options.disableTimeBudgetCap),
|
||||
getRemainingTimeMs: options.getRemainingTimeMs,
|
||||
maxResendRequests: remainingAutomaticResendCount,
|
||||
resendIntervalMs,
|
||||
|
||||
Reference in New Issue
Block a user