Merge branch 'master' into codex/upstream-pr

This commit is contained in:
dada2321
2026-04-20 16:04:13 +08:00
committed by GitHub
44 changed files with 4677 additions and 346 deletions
+17 -5
View File
@@ -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;
}
+39 -1
View File
@@ -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)) {
+689
View File
@@ -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,
};
});
+6
View File
@@ -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,
+89
View File
@@ -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,
+2 -2
View File
@@ -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,
+11 -21
View File
@@ -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);
+4 -1
View File
@@ -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
+1
View File
@@ -48,6 +48,7 @@
? await getOAuthFlowStepTimeoutMs(180000, {
step: 7,
actionLabel: 'OAuth 登录并进入验证码页',
oauthUrl,
})
: 180000;
+59 -3
View File
@@ -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,