diff --git a/README.md b/README.md index ffef8ff..2099fb0 100644 --- a/README.md +++ b/README.md @@ -132,7 +132,15 @@ 4. Step 1 会直接在 SUB2API 后台生成 OAuth 链接 5. Step 10 会把 localhost 回调提交回 SUB2API,并直接创建 OpenAI 账号 -### 方案 C:`Hotmail 账号池` +### 方案 C:`Codex2API + QQ / 163 / 163 VIP` + +1. `来源` 选择 `Codex2API` +2. 填好 `Codex2API` 后台地址、管理密钥 +3. `Mail` 与 `邮箱生成` 的配置方式同方案 A +4. Step 7 会直接通过 Codex2API 协议 `/api/admin/oauth/generate-auth-url` 生成 OAuth 链接 +5. Step 10 会把 localhost 回调中的 `code / state` 通过 `/api/admin/oauth/exchange-code` 直接提交给 Codex2API + +### 方案 D:`Hotmail 账号池` 1. `Mail` 选择 `Hotmail` 2. 在 `Hotmail 账号池` 中添加 `邮箱 / Client ID / Refresh Token` @@ -140,7 +148,7 @@ 4. 通过后再执行步骤或 `Auto` 5. 当前项目中,`Mail = Hotmail` 时会直接使用账号池里的邮箱作为注册邮箱,不再走 `Duck / Cloudflare` 自动生成 -### 方案 D:`2925 账号池` +### 方案 E:`2925 账号池` 1. `Mail` 选择 `2925` 2. 在 `2925 账号池` 中添加 `邮箱 / 密码` @@ -177,6 +185,20 @@ Step 1 和 Step 10 都依赖这个地址。 插件会在 Step 1 和 Step 10 自动从 `/api/v1/admin/proxies/all` 解析这个代理,并在 OAuth 链接生成、授权码交换和账号创建请求中附带 `proxy_id`。如果名称匹配到多个代理,请改填代理 ID;留空则不会发送 `proxy_id`。 +### `Codex2API` + +当 `来源 = Codex2API` 时,需要配置: + +- `Codex2API`:后台账号管理页地址,默认 `http://localhost:8080/admin/accounts` +- `管理密钥`:Codex2API 的 `Admin Secret` + +插件会在: + +- Step 7 调用 `POST /api/admin/oauth/generate-auth-url` 生成授权链接 +- Step 10 调用 `POST /api/admin/oauth/exchange-code` 完成 localhost callback 的授权码交换并创建账号 + +这条来源是协议直连,不依赖 Codex2API 后台页面的“添加账号 / OAuth 授权 / 生成授权链接”按钮 DOM。 + ### `Mail` 支持五种验证码来源: diff --git a/background.js b/background.js index 8c8eb1c..bbc85c7 100644 --- a/background.js +++ b/background.js @@ -156,6 +156,7 @@ const OAUTH_FLOW_TIMEOUT_MS = 5 * 60 * 1000; const SUB2API_STEP1_RESPONSE_TIMEOUT_MS = 90000; const SUB2API_STEP9_RESPONSE_TIMEOUT_MS = 120000; const DEFAULT_SUB2API_URL = 'https://sub2api.hisence.fun/admin/accounts'; +const DEFAULT_CODEX2API_URL = 'http://localhost:8080/admin/accounts'; const DEFAULT_SUB2API_GROUP_NAME = 'codex'; const DEFAULT_SUB2API_PROXY_NAME = ''; const DEFAULT_SUB2API_REDIRECT_URI = 'http://localhost:1455/auth/callback'; @@ -252,6 +253,8 @@ const PERSISTED_SETTING_DEFAULTS = { sub2apiPassword: '', sub2apiGroupName: DEFAULT_SUB2API_GROUP_NAME, sub2apiDefaultProxyName: DEFAULT_SUB2API_PROXY_NAME, + codex2apiUrl: DEFAULT_CODEX2API_URL, + codex2apiAdminKey: '', customPassword: '', autoRunSkipFailures: false, autoRunFallbackThreadIntervalMinutes: 0, @@ -282,6 +285,7 @@ const PERSISTED_SETTING_DEFAULTS = { cloudflareTempEmailAdminAuth: '', cloudflareTempEmailCustomAuth: '', cloudflareTempEmailReceiveMailbox: '', + cloudflareTempEmailUseRandomSubdomain: false, cloudflareTempEmailDomain: '', cloudflareTempEmailDomains: [], hotmailAccounts: [], @@ -329,6 +333,8 @@ const DEFAULT_STATE = { sub2apiGroupId: null, // SUB2API 目标分组 ID。 sub2apiDraftName: null, // SUB2API 本轮预生成的账号名称。 sub2apiProxyId: null, // SUB2API 本轮使用的代理 ID。 + codex2apiSessionId: null, // Codex2API OAuth 会话 ID。 + codex2apiOAuthState: null, // Codex2API OAuth state。 flowStartTime: null, // 当前流程开始时间。 tabRegistry: {}, // 程序维护的标签页注册表。 sourceLastUrls: {}, // 各来源页面最近一次打开的地址记录。 @@ -653,7 +659,14 @@ function normalizeEmailGenerator(value = '') { } function normalizePanelMode(value = '') { - return String(value || '').trim().toLowerCase() === 'sub2api' ? 'sub2api' : 'cpa'; + const normalized = String(value || '').trim().toLowerCase(); + if (normalized === 'sub2api') { + return 'sub2api'; + } + if (normalized === 'codex2api') { + return 'codex2api'; + } + return 'cpa'; } function normalizeMailProvider(value = '') { @@ -829,6 +842,7 @@ function getCloudflareTempEmailConfig(state = {}) { adminAuth: String(state.cloudflareTempEmailAdminAuth || ''), customAuth: String(state.cloudflareTempEmailCustomAuth || ''), receiveMailbox: normalizeCloudflareTempEmailReceiveMailbox(state.cloudflareTempEmailReceiveMailbox), + useRandomSubdomain: Boolean(state.cloudflareTempEmailUseRandomSubdomain), domain: normalizeCloudflareTempEmailDomain(state.cloudflareTempEmailDomain), domains: normalizeCloudflareTempEmailDomains(state.cloudflareTempEmailDomains), }; @@ -874,6 +888,10 @@ function normalizePersistentSettingValue(key, value) { return String(value || '').trim(); case 'sub2apiDefaultProxyName': return String(value || '').trim(); + case 'codex2apiUrl': + return normalizeCodex2ApiUrl(value); + case 'codex2apiAdminKey': + return String(value || '').trim(); case 'customPassword': return String(value || ''); case 'autoRunSkipFailures': @@ -897,6 +915,7 @@ function normalizePersistentSettingValue(key, value) { return normalizeEmailGenerator(value); case 'autoDeleteUsedIcloudAlias': case 'accountRunHistoryTextEnabled': + case 'cloudflareTempEmailUseRandomSubdomain': return Boolean(value); case 'icloudHostPreference': return normalizeIcloudHost(value) || 'auto'; @@ -3623,11 +3642,31 @@ function normalizeSub2ApiUrl(rawUrl) { return parsed.toString(); } +function normalizeCodex2ApiUrl(rawUrl) { + if (typeof navigationUtils !== 'undefined' && navigationUtils?.normalizeCodex2ApiUrl) { + return navigationUtils.normalizeCodex2ApiUrl(rawUrl); + } + const input = (rawUrl || '').trim() || DEFAULT_CODEX2API_URL; + const withProtocol = /^https?:\/\//i.test(input) ? input : `http://${input}`; + const parsed = new URL(withProtocol); + if (!parsed.pathname || parsed.pathname === '/' || parsed.pathname === '/admin') { + parsed.pathname = '/admin/accounts'; + } + parsed.hash = ''; + return parsed.toString(); +} + function getPanelMode(state = {}) { if (typeof navigationUtils !== 'undefined' && navigationUtils?.getPanelMode) { return navigationUtils.getPanelMode(state); } - return state.panelMode === 'sub2api' ? 'sub2api' : 'cpa'; + if (state.panelMode === 'sub2api') { + return 'sub2api'; + } + if (state.panelMode === 'codex2api') { + return 'codex2api'; + } + return 'cpa'; } function getPanelModeLabel(modeOrState) { @@ -3635,7 +3674,13 @@ function getPanelModeLabel(modeOrState) { return navigationUtils.getPanelModeLabel(modeOrState); } const mode = typeof modeOrState === 'string' ? modeOrState : getPanelMode(modeOrState); - return mode === 'sub2api' ? 'SUB2API' : 'CPA'; + if (mode === 'sub2api') { + return 'SUB2API'; + } + if (mode === 'codex2api') { + return 'Codex2API'; + } + return 'CPA'; } function isSignupPageHost(hostname = '') { @@ -3939,6 +3984,7 @@ function isRetryableContentScriptTransportError(error) { } const navigationUtils = self.MultiPageBackgroundNavigationUtils?.createNavigationUtils({ + DEFAULT_CODEX2API_URL, DEFAULT_SUB2API_URL, normalizeLocalCpaStep9Mode, }); @@ -4132,6 +4178,8 @@ function getDownstreamStateResets(step) { sub2apiOAuthState: null, sub2apiGroupId: null, sub2apiDraftName: null, + codex2apiSessionId: null, + codex2apiOAuthState: null, flowStartTime: null, password: null, lastEmailTimestamp: null, @@ -4917,6 +4965,8 @@ async function handleStepData(step, payload) { 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); } @@ -5448,6 +5498,7 @@ const mail2925SessionManager = self.MultiPageBackgroundMail2925Session?.createMa sleepWithStop, throwIfStopped, upsertMail2925AccountInList, + waitForTabComplete, waitForTabUrlMatch, }); @@ -6184,6 +6235,7 @@ const panelBridge = self.MultiPageBackgroundPanelBridge?.createPanelBridge({ closeConflictingTabsForSource, ensureContentScriptReadyOnTab, getPanelMode, + normalizeCodex2ApiUrl, normalizeSub2ApiUrl, rememberSourceLastUrl, sendToContentScript, @@ -6359,6 +6411,7 @@ const step10Executor = self.MultiPageBackgroundStep10?.createStep10Executor({ getTabId, isLocalhostOAuthCallbackUrl, isTabAlive, + normalizeCodex2ApiUrl, normalizeSub2ApiUrl, rememberSourceLastUrl, reuseOrCreateTab, @@ -6425,6 +6478,7 @@ const messageRouter = self.MultiPageBackgroundMessageRouter?.createMessageRouter getPendingAutoRunTimerPlan, getSourceLabel, getState, + getTabId, getStopRequested: () => stopRequested, handleCloudflareSecurityBlocked, handleAutoRunLoopUnhandledError, @@ -6436,6 +6490,7 @@ const messageRouter = self.MultiPageBackgroundMessageRouter?.createMessageRouter isLocalhostOAuthCallbackUrl, isLuckmailProvider, isStopError, + isTabAlive, launchAutoRunTimerPlan, listIcloudAliases, listLuckmailPurchasesForManagement, @@ -6807,7 +6862,7 @@ async function runPreStep6CookieCleanup() { async function refreshOAuthUrlBeforeStep6(state) { if (state?.contributionModeExpected && !state?.contributionMode) { - throw new Error('步骤 7:当前自动流程预期使用贡献模式,但运行态 contributionMode 已丢失,已阻止回退到普通 CPA / SUB2API 链路。请重新进入贡献模式后再点击自动。'); + throw new Error('步骤 7:当前自动流程预期使用贡献模式,但运行态 contributionMode 已丢失,已阻止回退到普通 CPA / SUB2API / Codex2API 链路。请重新进入贡献模式后再点击自动。'); } if (state?.contributionMode && contributionOAuthManager?.startContributionFlow) { await addLog('步骤 7:contributionMode=true,走公开贡献接口,正在申请 OAuth 登录地址...', 'info'); @@ -6823,7 +6878,7 @@ async function refreshOAuthUrlBeforeStep6(state) { await handleStepData(1, { oauthUrl }); return oauthUrl; } - await addLog(`步骤 7:contributionMode=false,走普通 CPA / SUB2API 链路(当前面板:${getPanelModeLabel(state)}),正在刷新 OAuth 登录地址...`, 'info'); + await addLog(`步骤 7:contributionMode=false,走普通 CPA / SUB2API / Codex2API 链路(当前面板:${getPanelModeLabel(state)}),正在刷新 OAuth 登录地址...`, 'info'); console.log(LOG_PREFIX, '[refreshOAuthUrlBeforeStep6] requesting fresh OAuth directly from panel'); const refreshResult = await requestOAuthUrlFromPanel(state, { logLabel: '步骤 7' }); await handleStepData(1, refreshResult); @@ -7524,7 +7579,7 @@ async function executeContributionStep10(state) { async function executeStep10(state) { if (state?.contributionModeExpected && !state?.contributionMode) { - throw new Error('步骤 10:当前自动流程预期使用贡献模式,但运行态 contributionMode 已丢失,已阻止回退到普通 CPA / SUB2API 提交。请重新进入贡献模式后再点击自动。'); + throw new Error('步骤 10:当前自动流程预期使用贡献模式,但运行态 contributionMode 已丢失,已阻止回退到普通 CPA / SUB2API / Codex2API 提交。请重新进入贡献模式后再点击自动。'); } if (state?.contributionMode) { return executeContributionStep10(state); diff --git a/background/generated-email-helpers.js b/background/generated-email-helpers.js index 3d924a7..7db5c98 100644 --- a/background/generated-email-helpers.js +++ b/background/generated-email-helpers.js @@ -146,6 +146,7 @@ const requestedName = String(options.localPart || options.name || '').trim().toLowerCase() || generateCloudflareAliasLocalPart(); const payload = { enablePrefix: true, + enableRandomSubdomain: Boolean(config.useRandomSubdomain), name: requestedName, domain: config.domain, }; diff --git a/background/mail-2925-session.js b/background/mail-2925-session.js index 410fa97..502362a 100644 --- a/background/mail-2925-session.js +++ b/background/mail-2925-session.js @@ -25,6 +25,7 @@ sleepWithStop, throwIfStopped, upsertMail2925AccountInList, + waitForTabComplete, waitForTabUrlMatch, } = deps; @@ -45,6 +46,9 @@ ]; const MAIL2925_LIMIT_ERROR_PREFIX = 'MAIL2925_LIMIT_REACHED::'; const MAIL2925_THREAD_TERMINATED_ERROR_PREFIX = 'MAIL2925_THREAD_TERMINATED::'; + const MAIL2925_LOGIN_MESSAGE_RETRY_WINDOW_MS = 15000; + const MAIL2925_LOGIN_RESPONSE_TIMEOUT_MS = 120000; + const MAIL2925_LOGIN_PAGE_RECOVERY_TIMEOUT_MS = 120000; function getMail2925MailConfig() { return { @@ -95,6 +99,14 @@ return getErrorMessage(error).startsWith(MAIL2925_THREAD_TERMINATED_ERROR_PREFIX); } + function isRetryableMail2925TransportError(error) { + const message = getErrorMessage(error).toLowerCase(); + return message.includes('receiving end does not exist') + || message.includes('message port closed') + || message.includes('content script on') + || message.includes('did not respond'); + } + async function syncMail2925Accounts(accounts) { const normalized = normalizeMail2925Accounts(accounts); await setPersistentSettings({ mail2925Accounts: normalized }); @@ -399,6 +411,43 @@ return removedCount; } + async function recoverMail2925LoginPageAfterTransportError(tabId) { + const numericTabId = Number(tabId); + if (!Number.isInteger(numericTabId) || numericTabId <= 0) { + return; + } + + const currentUrl = (await getMail2925TabUrlById(numericTabId)) || await getMail2925CurrentTabUrl(); + await addLog( + `2925:登录提交后页面发生跳转或重载,正在等待当前标签页恢复后继续确认登录态。当前地址:${currentUrl || 'unknown'}`, + 'warn' + ); + + if (typeof waitForTabComplete === 'function') { + const completedTab = await waitForTabComplete(numericTabId, { + timeoutMs: MAIL2925_LOGIN_PAGE_RECOVERY_TIMEOUT_MS, + retryDelayMs: 300, + }); + await addLog( + `2925:登录跳转等待结束,当前标签地址:${String(completedTab?.url || '').trim() || 'unknown'}`, + completedTab?.url ? 'info' : 'warn' + ); + } + + if (typeof ensureContentScriptReadyOnTab === 'function') { + await ensureContentScriptReadyOnTab(MAIL2925_SOURCE, numericTabId, { + inject: MAIL2925_INJECT, + injectSource: MAIL2925_INJECT_SOURCE, + timeoutMs: MAIL2925_LOGIN_PAGE_RECOVERY_TIMEOUT_MS, + retryDelayMs: 800, + logMessage: '步骤 0:2925 登录后页面仍在跳转,正在等待邮箱页重新就绪...', + }); + } + + const recoveredUrl = (await getMail2925TabUrlById(numericTabId)) || await getMail2925CurrentTabUrl(); + await addLog(`2925:登录跳转恢复后当前标签地址:${recoveredUrl || 'unknown'}`, 'info'); + } + async function ensureMail2925MailboxSession(options = {}) { const { accountId = null, @@ -520,10 +569,10 @@ } let result; - try { + const sendEnsureSessionRequest = async () => { const beforeSendUrl = (await getMail2925TabUrlById(tabId)) || await getMail2925CurrentTabUrl(); await addLog(`2925:发送 ENSURE_MAIL2925_SESSION 前当前地址:${beforeSendUrl || 'unknown'}`, 'info'); - result = await sendLoginMessage( + return sendLoginMessage( MAIL2925_SOURCE, { type: 'ENSURE_MAIL2925_SESSION', @@ -537,19 +586,34 @@ }, }, { - timeoutMs: 50000, + timeoutMs: MAIL2925_LOGIN_MESSAGE_RETRY_WINDOW_MS, retryDelayMs: 800, - responseTimeoutMs: 50000, + responseTimeoutMs: MAIL2925_LOGIN_RESPONSE_TIMEOUT_MS, logMessage: '步骤 0:2925 登录页通信异常,正在等待页面恢复...', } ); + }; + try { + result = await sendEnsureSessionRequest(); } catch (err) { - const message = `2925:${actionLabel}失败(${getErrorMessage(err) || '40 秒内未进入收件箱'})。`; - const stopped = await stopAutoRunForMail2925LoginFailure(`${message}已按手动停止逻辑暂停自动流程。`); - if (stopped) { - throw new Error('流程已被用户停止。'); + if (isRetryableMail2925TransportError(err)) { + try { + await recoverMail2925LoginPageAfterTransportError(tabId); + await addLog('2925:页面恢复完成,正在重新确认登录态...', 'info'); + result = await sendEnsureSessionRequest(); + } catch (recoveryErr) { + err = recoveryErr; + } + } + + if (!result) { + const message = `2925:${actionLabel}失败(${getErrorMessage(err) || '登录结果确认超时'})。`; + const stopped = await stopAutoRunForMail2925LoginFailure(`${message}已按手动停止逻辑暂停自动流程。`); + if (stopped) { + throw new Error('流程已被用户停止。'); + } + throw err; } - throw err; } if (result?.error) { @@ -584,6 +648,18 @@ await failMailboxSession(`2925:${actionLabel}失败,登录后仍未进入收件箱。`); } + if (!account) { + await addLog('2925:未触发自动登录,继续复用当前已登录会话。', 'info'); + return { + account: null, + mail: getMail2925MailConfig(), + result: { + ...result, + usedExistingSession: true, + }, + }; + } + await patchMail2925Account(account.id, { lastLoginAt: Date.now(), lastError: '', diff --git a/background/message-router.js b/background/message-router.js index b8580fd..c293b16 100644 --- a/background/message-router.js +++ b/background/message-router.js @@ -40,6 +40,7 @@ getPendingAutoRunTimerPlan, getSourceLabel, getState, + getTabId, getStopRequested, handleAutoRunLoopUnhandledError, importSettingsBundle, @@ -50,6 +51,7 @@ isLocalhostOAuthCallbackUrl, isLuckmailProvider, isStopError, + isTabAlive, launchAutoRunTimerPlan, listIcloudAliases, listLuckmailPurchasesForManagement, @@ -108,6 +110,23 @@ return appendAccountRunRecord(status, state, reason); } + async function ensureManualStepPrerequisites(step) { + if (step !== 4) { + return; + } + + const signupTabId = typeof getTabId === 'function' + ? await getTabId('signup-page') + : null; + const signupTabAlive = signupTabId && typeof isTabAlive === 'function' + ? await isTabAlive('signup-page') + : Boolean(signupTabId); + + if (!signupTabId || !signupTabAlive) { + throw new Error('手动执行步骤 4 前,请先执行步骤 1 或步骤 2,确保认证页仍然打开并停留在验证码页。'); + } + } + async function handleStepData(step, payload) { switch (step) { case 1: { @@ -121,6 +140,8 @@ 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); } @@ -402,6 +423,9 @@ await ensureManualInteractionAllowed('手动执行步骤'); } const step = message.payload.step; + if (message.source === 'sidepanel') { + await ensureManualStepPrerequisites(step); + } if (message.source === 'sidepanel') { await invalidateDownstreamAfterStepRestart(step, { logLabel: `步骤 ${step} 重新执行` }); } diff --git a/background/navigation-utils.js b/background/navigation-utils.js index 2c7b793..edde364 100644 --- a/background/navigation-utils.js +++ b/background/navigation-utils.js @@ -3,6 +3,7 @@ })(typeof self !== 'undefined' ? self : globalThis, function createBackgroundNavigationUtilsModule() { function createNavigationUtils(deps = {}) { const { + DEFAULT_CODEX2API_URL, DEFAULT_SUB2API_URL, normalizeLocalCpaStep9Mode, } = deps; @@ -27,13 +28,36 @@ return parsed.toString(); } + function normalizeCodex2ApiUrl(rawUrl) { + const input = (rawUrl || '').trim() || DEFAULT_CODEX2API_URL; + const withProtocol = /^https?:\/\//i.test(input) ? input : `http://${input}`; + const parsed = new URL(withProtocol); + if (!parsed.pathname || parsed.pathname === '/' || parsed.pathname === '/admin') { + parsed.pathname = '/admin/accounts'; + } + parsed.hash = ''; + return parsed.toString(); + } + function getPanelMode(state = {}) { - return state.panelMode === 'sub2api' ? 'sub2api' : 'cpa'; + if (state.panelMode === 'sub2api') { + return 'sub2api'; + } + if (state.panelMode === 'codex2api') { + return 'codex2api'; + } + return 'cpa'; } function getPanelModeLabel(modeOrState) { const mode = typeof modeOrState === 'string' ? modeOrState : getPanelMode(modeOrState); - return mode === 'sub2api' ? 'SUB2API' : 'CPA'; + if (mode === 'sub2api') { + return 'SUB2API'; + } + if (mode === 'codex2api') { + return 'Codex2API'; + } + return 'CPA'; } function isSignupPageHost(hostname = '') { @@ -124,6 +148,14 @@ || candidate.pathname.startsWith('/login') || candidate.pathname === '/' ); + case 'codex2api-panel': + return Boolean(reference) + && candidate.origin === reference.origin + && ( + candidate.pathname.startsWith('/admin/accounts') + || candidate.pathname === '/admin' + || candidate.pathname === '/' + ); default: return false; } @@ -160,6 +192,7 @@ isSignupPageHost, isSignupPasswordPageUrl, matchesSourceUrlFamily, + normalizeCodex2ApiUrl, normalizeSub2ApiUrl, parseUrlSafely, shouldBypassStep9ForLocalCpa, diff --git a/background/panel-bridge.js b/background/panel-bridge.js index ad97e6c..a667960 100644 --- a/background/panel-bridge.js +++ b/background/panel-bridge.js @@ -8,6 +8,7 @@ closeConflictingTabsForSource, ensureContentScriptReadyOnTab, getPanelMode, + normalizeCodex2ApiUrl, normalizeSub2ApiUrl, rememberSourceLastUrl, sendToContentScript, @@ -17,7 +18,74 @@ SUB2API_STEP1_RESPONSE_TIMEOUT_MS, } = deps; + function normalizeAdminKey(value = '') { + return String(value || '').trim(); + } + + function extractStateFromAuthUrl(authUrl = '') { + try { + return new URL(authUrl).searchParams.get('state') || ''; + } catch { + return ''; + } + } + + function getCodex2ApiErrorMessage(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 || `Codex2API 请求失败(HTTP ${responseStatus})。`; + } + + async function fetchCodex2ApiJson(origin, path, options = {}) { + const timeoutMs = Math.max(1000, Math.floor(Number(options.timeoutMs) || 30000)); + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), timeoutMs); + + try { + const response = await fetch(`${origin}${path}`, { + method: options.method || 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + 'X-Admin-Key': normalizeAdminKey(options.adminKey), + }, + 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(getCodex2ApiErrorMessage(payload, response.status)); + } + + return payload; + } catch (error) { + if (error?.name === 'AbortError') { + throw new Error('Codex2API 请求超时,请稍后重试。'); + } + throw error; + } finally { + clearTimeout(timer); + } + } + async function requestOAuthUrlFromPanel(state, options = {}) { + if (getPanelMode(state) === 'codex2api') { + return requestCodex2ApiOAuthUrl(state, options); + } if (getPanelMode(state) === 'sub2api') { return requestSub2ApiOAuthUrl(state, options); } @@ -74,6 +142,39 @@ return result || {}; } + async function requestCodex2ApiOAuthUrl(state, options = {}) { + const { logLabel = 'OAuth 刷新' } = options; + const codex2apiUrl = normalizeCodex2ApiUrl(state.codex2apiUrl); + const adminKey = normalizeAdminKey(state.codex2apiAdminKey); + + if (!adminKey) { + throw new Error('尚未配置 Codex2API 管理密钥,请先在侧边栏填写。'); + } + + const origin = new URL(codex2apiUrl).origin; + await addLog(`${logLabel}:正在通过 Codex2API 协议生成 OAuth 授权链接...`); + + const result = await fetchCodex2ApiJson(origin, '/api/admin/oauth/generate-auth-url', { + adminKey, + method: 'POST', + body: {}, + }); + + const oauthUrl = String(result?.auth_url || result?.authUrl || '').trim(); + const sessionId = String(result?.session_id || result?.sessionId || '').trim(); + const oauthState = extractStateFromAuthUrl(oauthUrl); + + if (!oauthUrl || !sessionId) { + throw new Error('Codex2API 未返回有效的 auth_url 或 session_id。'); + } + + return { + oauthUrl, + codex2apiSessionId: sessionId, + codex2apiOAuthState: oauthState || null, + }; + } + async function requestSub2ApiOAuthUrl(state, options = {}) { const { logLabel = 'OAuth 刷新' } = options; const sub2apiUrl = normalizeSub2ApiUrl(state.sub2apiUrl); @@ -135,6 +236,7 @@ return { requestOAuthUrlFromPanel, + requestCodex2ApiOAuthUrl, requestCpaOAuthUrl, requestSub2ApiOAuthUrl, }; diff --git a/background/steps/fetch-login-code.js b/background/steps/fetch-login-code.js index 8bf53c4..7baad00 100644 --- a/background/steps/fetch-login-code.js +++ b/background/steps/fetch-login-code.js @@ -9,6 +9,7 @@ chrome, CLOUDFLARE_TEMP_EMAIL_PROVIDER, confirmCustomVerificationStepBypass, + ensureMail2925MailboxSession, ensureStep8VerificationPageReady, getOAuthFlowRemainingMs, getOAuthFlowStepTimeoutMs, @@ -57,6 +58,20 @@ return String(value || '').trim().toLowerCase(); } + function getExpectedMail2925MailboxEmail(state = {}) { + if (Boolean(state?.mail2925UseAccountPool)) { + const currentAccountId = String(state?.currentMail2925AccountId || '').trim(); + const accounts = Array.isArray(state?.mail2925Accounts) ? state.mail2925Accounts : []; + const currentAccount = accounts.find((account) => String(account?.id || '') === currentAccountId) || null; + const accountEmail = String(currentAccount?.email || '').trim().toLowerCase(); + if (accountEmail) { + return accountEmail; + } + } + + return String(state?.mail2925BaseEmail || '').trim().toLowerCase(); + } + async function focusOrOpenMailTab(mail) { const alive = await isTabAlive(mail.source); if (alive) { @@ -134,7 +149,17 @@ await addLog(`步骤 8:正在通过 ${mail.label} 轮询验证码...`); } else { await addLog(`步骤 8:正在打开${mail.label}...`); - await focusOrOpenMailTab(mail); + if (mail.provider === '2925' && typeof ensureMail2925MailboxSession === 'function') { + await ensureMail2925MailboxSession({ + accountId: state.currentMail2925AccountId || null, + forceRelogin: false, + allowLoginWhenOnLoginPage: Boolean(state?.mail2925UseAccountPool), + expectedMailboxEmail: getExpectedMail2925MailboxEmail(state), + actionLabel: 'Step 8: ensure 2925 mailbox session', + }); + } else { + await focusOrOpenMailTab(mail); + } if (mail.provider === '2925') { await addLog(`步骤 8:将直接使用当前已登录的 ${mail.label} 轮询验证码。`, 'info'); } diff --git a/background/steps/fetch-signup-code.js b/background/steps/fetch-signup-code.js index 066baee..77dbeaa 100644 --- a/background/steps/fetch-signup-code.js +++ b/background/steps/fetch-signup-code.js @@ -72,7 +72,7 @@ const signupTabId = await getTabId('signup-page'); if (!signupTabId) { - throw new Error('认证页面标签页已关闭,无法继续步骤 4。'); + throw new Error('认证页面标签页已关闭,无法继续步骤 4。请先执行步骤 1 或步骤 2,重新打开认证页后再试。'); } await chrome.tabs.update(signupTabId, { active: true }); diff --git a/background/steps/oauth-login.js b/background/steps/oauth-login.js index 596ac9f..0d1c9e8 100644 --- a/background/steps/oauth-login.js +++ b/background/steps/oauth-login.js @@ -23,6 +23,20 @@ throwIfStopped, } = deps; + function isManagementSecretConfigError(error) { + const message = String(typeof error === 'string' ? error : error?.message || '').trim(); + if (!message) { + return false; + } + + const mentionsSecret = /管理密钥|Admin Secret|X-Admin-Key|CPA Key/i.test(message); + if (!mentionsSecret) { + return false; + } + + return /缺少|未配置|请输入|无效|错误|失败|401|认证失败|未授权|unauthorized|invalid/i.test(message); + } + async function executeStep7(state) { if (!state.email) { throw new Error('缺少邮箱地址,请先完成步骤 3。'); @@ -99,6 +113,13 @@ if (isAddPhoneAuthFailure(err)) { throw err; } + if (isManagementSecretConfigError(err)) { + await addLog( + `步骤 7:检测到来源后台管理密钥缺失或错误,不再重试,当前流程停止。原因:${getErrorMessage(err)}`, + 'error' + ); + throw err; + } lastError = err; if (attempt >= STEP6_MAX_ATTEMPTS) { break; diff --git a/background/steps/platform-verify.js b/background/steps/platform-verify.js index 97bc10d..d4e01b4 100644 --- a/background/steps/platform-verify.js +++ b/background/steps/platform-verify.js @@ -12,6 +12,7 @@ getTabId, isLocalhostOAuthCallbackUrl, isTabAlive, + normalizeCodex2ApiUrl, normalizeSub2ApiUrl, rememberSourceLastUrl, reuseOrCreateTab, @@ -21,7 +22,86 @@ SUB2API_STEP9_RESPONSE_TIMEOUT_MS, } = deps; + function normalizeString(value = '') { + return String(value || '').trim(); + } + + function parseLocalhostCallback(rawUrl) { + let parsed; + try { + parsed = new URL(rawUrl); + } catch { + 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('步骤 10 捕获到的 localhost OAuth 回调地址缺少 code 或 state,请重新执行步骤 9。'); + } + + return { + url: parsed.toString(), + code, + state, + }; + } + + function getCodex2ApiErrorMessage(payload, responseStatus = 500) { + const details = [ + payload?.error, + payload?.message, + payload?.detail, + payload?.reason, + ] + .map((value) => normalizeString(value)) + .find(Boolean); + return details || `Codex2API 请求失败(HTTP ${responseStatus})。`; + } + + async function fetchCodex2ApiJson(origin, path, options = {}) { + const controller = new AbortController(); + const timeoutMs = Math.max(1000, Math.floor(Number(options.timeoutMs) || 30000)); + const timer = setTimeout(() => controller.abort(), timeoutMs); + + try { + const response = await fetch(`${origin}${path}`, { + method: options.method || 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + 'X-Admin-Key': normalizeString(options.adminKey), + }, + 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(getCodex2ApiErrorMessage(payload, response.status)); + } + + return payload; + } catch (error) { + if (error?.name === 'AbortError') { + throw new Error('Codex2API 请求超时,请稍后重试。'); + } + throw error; + } finally { + clearTimeout(timer); + } + } + async function executeStep10(state) { + if (getPanelMode(state) === 'codex2api') { + return executeCodex2ApiStep10(state); + } if (getPanelMode(state) === 'sub2api') { return executeSub2ApiStep10(state); } @@ -90,6 +170,48 @@ } } + async function executeCodex2ApiStep10(state) { + if (state.localhostUrl && !isLocalhostOAuthCallbackUrl(state.localhostUrl)) { + throw new Error('步骤 9 捕获到的 localhost OAuth 回调地址无效,请重新执行步骤 9。'); + } + if (!state.localhostUrl) { + throw new Error('缺少 localhost 回调地址,请先完成步骤 9。'); + } + if (!state.codex2apiSessionId) { + throw new Error('缺少 Codex2API 会话信息,请重新执行步骤 7。'); + } + if (!normalizeString(state.codex2apiAdminKey)) { + throw new Error('尚未配置 Codex2API 管理密钥,请先在侧边栏填写。'); + } + + const callback = parseLocalhostCallback(state.localhostUrl); + const expectedState = normalizeString(state.codex2apiOAuthState); + if (expectedState && expectedState !== callback.state) { + throw new Error('Codex2API 回调 state 与当前授权会话不匹配,请重新执行步骤 7。'); + } + + const codex2apiUrl = normalizeCodex2ApiUrl(state.codex2apiUrl); + const origin = new URL(codex2apiUrl).origin; + + await addLog('步骤 10:正在向 Codex2API 提交回调并创建账号...'); + const result = await fetchCodex2ApiJson(origin, '/api/admin/oauth/exchange-code', { + adminKey: state.codex2apiAdminKey, + method: 'POST', + body: { + session_id: state.codex2apiSessionId, + code: callback.code, + state: callback.state, + }, + }); + + const verifiedStatus = normalizeString(result?.message) || 'Codex2API OAuth 账号添加成功'; + await addLog(`步骤 10:${verifiedStatus}`, 'ok'); + await completeStepFromBackground(10, { + localhostUrl: callback.url, + verifiedStatus, + }); + } + async function executeSub2ApiStep10(state) { if (state.localhostUrl && !isLocalhostOAuthCallbackUrl(state.localhostUrl)) { throw new Error('步骤 9 捕获到的 localhost OAuth 回调地址无效,请重新执行步骤 9。'); @@ -161,6 +283,7 @@ return { executeCpaStep10, + executeCodex2ApiStep10, executeStep10, executeSub2ApiStep10, }; diff --git a/content/mail-163.js b/content/mail-163.js index dd7ba4f..edab71b 100644 --- a/content/mail-163.js +++ b/content/mail-163.js @@ -69,15 +69,141 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { // Find mail items // ============================================================ -function findMailItems() { - return document.querySelectorAll('div[sign="letter"]'); +function normalizeText(value) { + return String(value || '').replace(/\s+/g, ' ').trim(); } -function getCurrentMailIds() { +function isVisibleNode(node) { + if (!node) return false; + if (node.hidden) return false; + + const style = typeof window.getComputedStyle === 'function' + ? window.getComputedStyle(node) + : null; + if (style && (style.display === 'none' || style.visibility === 'hidden')) { + return false; + } + + const rect = typeof node.getBoundingClientRect === 'function' + ? node.getBoundingClientRect() + : null; + if (rect && rect.width <= 0 && rect.height <= 0) { + return false; + } + + return true; +} + +function isLikelyMailItemNode(node) { + if (!isVisibleNode(node)) { + return false; + } + if (node.matches?.('div[sign="letter"]')) { + return true; + } + if (node.querySelector?.('.nui-user, span.da0, [sign="trash"], [title="删除邮件"], [class*="subject"], [class*="sender"]')) { + return true; + } + + const summaryText = normalizeText( + node.getAttribute?.('aria-label') + || node.getAttribute?.('title') + || node.textContent + ); + if (!summaryText) { + return false; + } + + return /发件人|验证码|verification|chatgpt|openai|code/i.test(summaryText); +} + +function findMailItems() { + const selectorGroups = [ + 'div[sign="letter"]', + '[role="option"][aria-label]', + '[role="listitem"][aria-label]', + 'tr[aria-label]', + 'li[aria-label]', + 'div[aria-label]', + ]; + + for (const selector of selectorGroups) { + const matches = Array.from(document.querySelectorAll(selector)).filter(isLikelyMailItemNode); + if (matches.length > 0) { + return matches; + } + } + + return []; +} + +function getMailTextBySelectors(item, selectors = []) { + for (const selector of selectors) { + const candidates = item.querySelectorAll(selector); + for (const candidate of candidates) { + const texts = [ + candidate.getAttribute?.('title'), + candidate.getAttribute?.('aria-label'), + candidate.textContent, + ]; + for (const text of texts) { + const normalized = normalizeText(text); + if (normalized) { + return normalized; + } + } + } + } + return ''; +} + +function getMailSenderText(item) { + return getMailTextBySelectors(item, [ + '.nui-user', + '[class*="sender"]', + '[class*="from"]', + '[data-sender]', + ]); +} + +function getMailSubjectText(item) { + return getMailTextBySelectors(item, [ + 'span.da0', + '[class*="subject"]', + '[data-subject]', + 'strong', + ]); +} + +function getMailRowText(item) { + const ariaLabel = normalizeText(item.getAttribute('aria-label')); + const sender = getMailSenderText(item); + const subject = getMailSubjectText(item); + const fullText = normalizeText(item.textContent || ''); + return normalizeText([ariaLabel, sender, subject, fullText].filter(Boolean).join(' ')); +} + +function getMailItemId(item, index = 0) { + const candidates = [ + item.getAttribute('id'), + item.getAttribute('data-id'), + item.dataset?.id, + item.getAttribute('data-key'), + item.getAttribute('key'), + ].filter(Boolean); + + if (candidates.length > 0) { + return String(candidates[0]); + } + + return `${index}|${getMailRowText(item).slice(0, 240)}`; +} + +function getCurrentMailIds(items = []) { const ids = new Set(); - findMailItems().forEach(item => { - const id = item.getAttribute('id') || ''; - if (id) ids.add(id); + const sourceItems = items.length > 0 ? items : findMailItems(); + sourceItems.forEach((item, index) => { + ids.add(getMailItemId(item, index)); }); return ids; } @@ -90,7 +216,7 @@ function normalizeMinuteTimestamp(timestamp) { } function parseMail163Timestamp(rawText) { - const text = (rawText || '').replace(/\s+/g, ' ').trim(); + const text = normalizeText(rawText); if (!text) return null; let match = text.match(/(\d{4})年(\d{1,2})月(\d{1,2})日\s+(\d{1,2}):(\d{2})/); @@ -107,6 +233,37 @@ function parseMail163Timestamp(rawText) { ).getTime(); } + match = text.match(/今天\s*(\d{1,2}):(\d{2})/); + if (match) { + const [, hour, minute] = match; + const now = new Date(); + return new Date( + now.getFullYear(), + now.getMonth(), + now.getDate(), + Number(hour), + Number(minute), + 0, + 0 + ).getTime(); + } + + match = text.match(/昨天\s*(\d{1,2}):(\d{2})/); + if (match) { + const [, hour, minute] = match; + const now = new Date(); + now.setDate(now.getDate() - 1); + return new Date( + now.getFullYear(), + now.getMonth(), + now.getDate(), + Number(hour), + Number(minute), + 0, + 0 + ).getTime(); + } + match = text.match(/\b(\d{1,2}):(\d{2})\b/); if (match) { const [, hour, minute] = match; @@ -125,18 +282,48 @@ function parseMail163Timestamp(rawText) { return null; } -function getMailTimestamp(item) { - const candidates = []; - const timeCell = item.querySelector('.e00[title], [title*="年"][title*=":"]'); - if (timeCell?.getAttribute('title')) candidates.push(timeCell.getAttribute('title')); - if (timeCell?.textContent) candidates.push(timeCell.textContent); +function isLikelyMailTimestampText(value) { + const text = normalizeText(value); + if (!text) { + return false; + } + return /(\d{4}年\d{1,2}月\d{1,2}日\s+\d{1,2}:\d{2})|今天\s*\d{1,2}:\d{2}|昨天\s*\d{1,2}:\d{2}|\b\d{1,2}:\d{2}\b/.test(text); +} - const titledNodes = item.querySelectorAll('[title]'); - titledNodes.forEach((node) => { - const title = node.getAttribute('title'); - if (title) candidates.push(title); +function collectMailTimestampCandidates(item) { + const candidates = []; + const seen = new Set(); + const pushCandidate = (value) => { + const normalized = normalizeText(value); + if (!normalized || !isLikelyMailTimestampText(normalized) || seen.has(normalized)) { + return; + } + seen.add(normalized); + candidates.push(normalized); + }; + + const priorityNodes = item.querySelectorAll('.e00, [title], [aria-label], time, [class*="time"], [class*="date"]'); + priorityNodes.forEach((node) => { + pushCandidate(node.getAttribute?.('title')); + pushCandidate(node.getAttribute?.('aria-label')); + pushCandidate(node.textContent); }); + const textNodes = item.querySelectorAll('span, div, td, strong, b'); + textNodes.forEach((node) => { + const text = normalizeText(node.textContent); + if (text && text.length <= 24) { + pushCandidate(text); + } + }); + + pushCandidate(item.getAttribute('aria-label')); + pushCandidate(item.getAttribute('title')); + return candidates; +} + +function getMailTimestamp(item) { + const candidates = collectMailTimestampCandidates(item); for (const candidate of candidates) { const parsed = parseMail163Timestamp(candidate); if (parsed) return parsed; @@ -145,6 +332,134 @@ function getMailTimestamp(item) { return null; } +function collectOpenedMailTextCandidates() { + const texts = []; + const seen = new Set(); + const pushText = (value) => { + const normalized = normalizeText(value); + if (!normalized || seen.has(normalized)) { + return; + } + seen.add(normalized); + texts.push(normalized); + }; + + const selectors = [ + '.readHtml', + '[class*="readmail"]', + '[class*="mailread"]', + '[class*="mailBody"]', + '[class*="mailbody"]', + '[class*="mail-content"]', + '[class*="mailContent"]', + '[class*="mail-detail"]', + '[class*="mailDetail"]', + '[class*="detail"] [class*="content"]', + '[class*="read"] [class*="content"]', + '[role="main"]', + ]; + + selectors.forEach((selector) => { + document.querySelectorAll(selector).forEach((node) => { + pushText(node.innerText || node.textContent); + }); + }); + + document.querySelectorAll('iframe').forEach((frame) => { + try { + pushText(frame.contentDocument?.body?.innerText || frame.contentDocument?.body?.textContent); + } catch { + // Ignore cross-frame access errors and keep trying other candidates. + } + }); + + pushText(document.body?.innerText || document.body?.textContent); + return texts.sort((a, b) => b.length - a.length); +} + +function selectOpenedMailTextCandidate(item, candidates = [], options = {}) { + const subject = normalizeText(getMailSubjectText(item)).toLowerCase(); + const sender = normalizeText(getMailSenderText(item)).toLowerCase(); + const excludedSet = new Set((options.excludedTexts || []).map((value) => normalizeText(value))); + const allowExcludedFallback = options.allowExcludedFallback !== false; + + const pickCandidate = (source) => source.find((candidate) => { + const lower = candidate.toLowerCase(); + if (subject && lower.includes(subject)) { + return true; + } + if (sender && lower.includes(sender)) { + return true; + } + return Boolean(extractVerificationCode(candidate) && /chatgpt|openai|verification|验证码|login code/i.test(lower)); + }) || source[0] || ''; + + const filteredCandidates = candidates.filter((candidate) => !excludedSet.has(normalizeText(candidate))); + const preferred = pickCandidate(filteredCandidates); + if (preferred || !allowExcludedFallback) { + return preferred; + } + + return pickCandidate(candidates); +} + +function readOpenedMailText(item, options = {}) { + const candidates = collectOpenedMailTextCandidates(); + return selectOpenedMailTextCandidate(item, candidates, options); +} + +async function returnToInbox() { + const inboxLink = document.querySelector('.nui-tree-item-text[title="收件箱"], [title="收件箱"]'); + if (inboxLink) { + if (typeof simulateClick === 'function') { + simulateClick(inboxLink); + } else { + inboxLink.click(); + } + } + + for (let i = 0; i < 20; i += 1) { + if (findMailItems().length > 0) { + return true; + } + await sleep(250); + } + + return false; +} + +async function openMailAndGetMessageText(item) { + const beforeCandidates = collectOpenedMailTextCandidates(); + const beforeText = selectOpenedMailTextCandidate(item, beforeCandidates); + if (typeof simulateClick === 'function') { + simulateClick(item); + } else { + item.click(); + } + + let openedText = ''; + for (let i = 0; i < 24; i += 1) { + await sleep(250); + const candidate = readOpenedMailText(item, { + excludedTexts: beforeCandidates, + allowExcludedFallback: false, + }); + if (!candidate) { + continue; + } + openedText = candidate; + if (extractVerificationCode(candidate)) { + break; + } + if (candidate !== beforeText && candidate.length > beforeText.length + 24) { + break; + } + } + + await returnToInbox(); + return openedText; +} + function scheduleEmailCleanup(item, step) { setTimeout(() => { Promise.resolve(deleteEmail(item, step)).catch(() => { @@ -199,7 +514,7 @@ async function handlePollEmail(step, payload) { log(`步骤 ${step}:邮件列表已加载,共 ${items.length} 封邮件`); // Snapshot existing mail IDs - const existingMailIds = getCurrentMailIds(); + const existingMailIds = getCurrentMailIds(items); log(`步骤 ${step}:已记录当前 ${existingMailIds.size} 封旧邮件快照`); const FALLBACK_AFTER = 3; @@ -215,38 +530,58 @@ async function handlePollEmail(step, payload) { const allItems = findMailItems(); const useFallback = attempt > FALLBACK_AFTER; - for (const item of allItems) { - const id = item.getAttribute('id') || ''; + for (let index = 0; index < allItems.length; index++) { + const item = allItems[index]; + const id = getMailItemId(item, index); const mailTimestamp = getMailTimestamp(item); const mailMinute = normalizeMinuteTimestamp(mailTimestamp || 0); const passesTimeFilter = !filterAfterMinute || (mailMinute && mailMinute >= filterAfterMinute); - const shouldBypassOldSnapshot = Boolean(filterAfterMinute && passesTimeFilter && mailMinute > 0); if (!passesTimeFilter) { continue; } - if (!useFallback && !shouldBypassOldSnapshot && existingMailIds.has(id)) continue; + if (!useFallback && existingMailIds.has(id)) { + continue; + } - const senderEl = item.querySelector('.nui-user'); - const sender = senderEl ? senderEl.textContent.toLowerCase() : ''; + const sender = getMailSenderText(item).toLowerCase(); + const subject = getMailSubjectText(item); + const rowText = getMailRowText(item); + const ariaLabel = normalizeText(item.getAttribute('aria-label')).toLowerCase(); + const combinedText = normalizeText([subject, ariaLabel, rowText].filter(Boolean).join(' ')); - const subjectEl = item.querySelector('span.da0'); - const subject = subjectEl ? subjectEl.textContent : ''; + if (!mailTimestamp) { + log(`步骤 ${step}:邮件 ${id.slice(0, 60)} 未读取到时间,已跳过时间窗口校验后的文本匹配阶段。`, 'info'); + } - const ariaLabel = (item.getAttribute('aria-label') || '').toLowerCase(); - - const senderMatch = senderFilters.some(f => sender.includes(f.toLowerCase()) || ariaLabel.includes(f.toLowerCase())); - const subjectMatch = subjectFilters.some(f => subject.toLowerCase().includes(f.toLowerCase()) || ariaLabel.includes(f.toLowerCase())); + const senderMatch = senderFilters.some((filter) => { + const normalizedFilter = String(filter || '').toLowerCase(); + return normalizedFilter && (sender.includes(normalizedFilter) || ariaLabel.includes(normalizedFilter) || rowText.toLowerCase().includes(normalizedFilter)); + }); + const subjectMatch = subjectFilters.some((filter) => { + const normalizedFilter = String(filter || '').toLowerCase(); + return normalizedFilter && (subject.toLowerCase().includes(normalizedFilter) || ariaLabel.includes(normalizedFilter) || rowText.toLowerCase().includes(normalizedFilter)); + }); if (senderMatch || subjectMatch) { - const code = extractVerificationCode(subject + ' ' + ariaLabel); + let code = extractVerificationCode(combinedText); + let codeSource = '邮件列表'; + + if (!code) { + const openedText = await openMailAndGetMessageText(item); + code = extractVerificationCode(openedText); + if (code) { + codeSource = '邮件正文'; + } + } + if (code && excludedCodeSet.has(code)) { log(`步骤 ${step}:跳过排除的验证码:${code}`, 'info'); } else if (code && !seenCodes.has(code)) { seenCodes.add(code); persistSeenCodes(); - const source = useFallback && existingMailIds.has(id) ? '回退匹配邮件' : '新邮件'; + const source = useFallback && existingMailIds.has(id) ? `回退匹配${codeSource}` : `新邮件${codeSource}`; const timeLabel = mailTimestamp ? `,时间:${new Date(mailTimestamp).toLocaleString('zh-CN', { hour12: false })}` : ''; log(`步骤 ${step}:已找到验证码:${code}(来源:${source}${timeLabel},主题:${subject.slice(0, 40)})`, 'ok'); diff --git a/content/mail-2925.js b/content/mail-2925.js index 09e9581..4263f87 100644 --- a/content/mail-2925.js +++ b/content/mail-2925.js @@ -534,6 +534,9 @@ function detectMail2925ViewState() { function getMail2925DisplayedMailboxEmail() { const directSelectors = [ + '.right-header', + '[class~="right-header"]', + '[class*="right-header"]', '[class*="user"] [class*="mail"]', '[class*="user"] [class*="email"]', '[class*="account"] [class*="mail"]', @@ -548,7 +551,8 @@ function getMail2925DisplayedMailboxEmail() { if (!isVisibleNode(candidate) || isMailItemNode(candidate)) { continue; } - const email = extractEmails(candidate.textContent || candidate.innerText || '')[0] || ''; + const email = extractEmails(candidate.textContent || candidate.innerText || '') + .find((value) => /@2925\.com$/i.test(String(value || '').trim())) || ''; if (email) { return email; } @@ -567,7 +571,8 @@ function getMail2925DisplayedMailboxEmail() { return rect.top >= 0 && rect.top <= Math.max(window.innerHeight * 0.35, 280); }) .map((node) => { - const email = extractEmails(node.textContent || node.innerText || '')[0] || ''; + const email = extractEmails(node.textContent || node.innerText || '') + .find((value) => /@2925\.com$/i.test(String(value || '').trim())) || ''; return { node, email }; }) .filter((entry) => entry.email); diff --git a/content/signup-page.js b/content/signup-page.js index 53881b7..0b74b3e 100644 --- a/content/signup-page.js +++ b/content/signup-page.js @@ -1621,8 +1621,13 @@ function createStep6RecoverableResult(reason, snapshot, options = {}) { }; } -async function createStep6LoginTimeoutRecoverableResult(reason, snapshot, message) { - const resolvedSnapshot = normalizeStep6Snapshot(snapshot || inspectLoginAuthState()); +async function createStep6LoginTimeoutRecoveryTransition(reason, snapshot, message, options = {}) { + const { + loginVerificationRequestedAt = null, + via = 'login_timeout_recovered', + } = options; + let resolvedSnapshot = normalizeStep6Snapshot(snapshot || inspectLoginAuthState()); + let recovered = false; if (resolvedSnapshot?.state === 'login_timeout_error_page') { try { const recoveryResult = await recoverCurrentAuthRetryPage({ @@ -1631,8 +1636,9 @@ async function createStep6LoginTimeoutRecoverableResult(reason, snapshot, messag step: 7, timeoutMs: 12000, }); - if (recoveryResult?.recovered) { - log('步骤 7:登录超时报错页已点击“重试”,准备重新执行当前步骤。', 'warn'); + recovered = Boolean(recoveryResult?.recovered); + if (recovered) { + log('步骤 7:登录超时报错页已点击“重试”,正在按恢复后的页面状态继续当前流程。', 'warn'); } } catch (error) { if (/CF_SECURITY_BLOCKED::/i.test(String(error?.message || error || ''))) { @@ -1642,7 +1648,46 @@ async function createStep6LoginTimeoutRecoverableResult(reason, snapshot, messag } } - return createStep6RecoverableResult(reason, resolvedSnapshot, { + resolvedSnapshot = recovered + ? normalizeStep6Snapshot(await waitForKnownLoginAuthState(4000)) + : normalizeStep6Snapshot(inspectLoginAuthState()); + + if (resolvedSnapshot.state === 'verification_page') { + return { + action: 'done', + result: createStep6SuccessResult(resolvedSnapshot, { + via, + loginVerificationRequestedAt, + }), + }; + } + + if (resolvedSnapshot.state === 'password_page') { + log('步骤 7:登录超时报错页恢复后已进入密码页,继续当前登录流程。', 'warn'); + return { action: 'password', snapshot: resolvedSnapshot }; + } + + if (resolvedSnapshot.state === 'email_page') { + log('步骤 7:登录超时报错页恢复后已回到邮箱输入页,继续当前登录流程。', 'warn'); + return { action: 'email', snapshot: resolvedSnapshot }; + } + + return { + action: 'recoverable', + result: createStep6RecoverableResult(reason, resolvedSnapshot, { + message, + loginVerificationRequestedAt, + }), + }; +} + +async function createStep6LoginTimeoutRecoverableResult(reason, snapshot, message) { + const transition = await createStep6LoginTimeoutRecoveryTransition(reason, snapshot, message); + if (transition?.action === 'done' || transition?.action === 'recoverable') { + return transition.result; + } + + return createStep6RecoverableResult(reason, transition?.snapshot || normalizeStep6Snapshot(inspectLoginAuthState()), { message, }); } @@ -2222,13 +2267,30 @@ async function waitForStep6EmailSubmitTransition(emailSubmittedAt, timeout = 120 } if (snapshot.state === 'login_timeout_error_page') { + const transition = await createStep6LoginTimeoutRecoveryTransition( + 'login_timeout_error_page', + snapshot, + '提交邮箱后进入登录超时报错页。', + { + loginVerificationRequestedAt: emailSubmittedAt, + via: 'email_submit_timeout_recovered', + } + ); + if (transition.action === 'done') { + return { + action: 'done', + result: transition.result, + }; + } + if (transition.action === 'password') { + return { action: 'password', snapshot: transition.snapshot }; + } + if (transition.action === 'email') { + return { action: 'email', snapshot: transition.snapshot }; + } return { action: 'recoverable', - result: await createStep6LoginTimeoutRecoverableResult( - 'login_timeout_error_page', - snapshot, - '提交邮箱后进入登录超时报错页。' - ), + result: transition.result, }; } @@ -2257,13 +2319,30 @@ async function waitForStep6EmailSubmitTransition(emailSubmittedAt, timeout = 120 return { action: 'password', snapshot }; } if (snapshot.state === 'login_timeout_error_page') { + const transition = await createStep6LoginTimeoutRecoveryTransition( + 'login_timeout_error_page', + snapshot, + '提交邮箱后进入登录超时报错页。', + { + loginVerificationRequestedAt: emailSubmittedAt, + via: 'email_submit_timeout_recovered', + } + ); + if (transition.action === 'done') { + return { + action: 'done', + result: transition.result, + }; + } + if (transition.action === 'password') { + return { action: 'password', snapshot: transition.snapshot }; + } + if (transition.action === 'email') { + return { action: 'email', snapshot: transition.snapshot }; + } return { action: 'recoverable', - result: await createStep6LoginTimeoutRecoverableResult( - 'login_timeout_error_page', - snapshot, - '提交邮箱后进入登录超时报错页。' - ), + result: transition.result, }; } if (snapshot.state === 'oauth_consent_page') { @@ -2300,13 +2379,30 @@ async function waitForStep6PasswordSubmitTransition(passwordSubmittedAt, timeout } if (snapshot.state === 'login_timeout_error_page') { + const transition = await createStep6LoginTimeoutRecoveryTransition( + 'login_timeout_error_page', + snapshot, + '提交密码后进入登录超时报错页。', + { + loginVerificationRequestedAt: passwordSubmittedAt, + via: 'password_submit_timeout_recovered', + } + ); + if (transition.action === 'done') { + return { + action: 'done', + result: transition.result, + }; + } + if (transition.action === 'password') { + return { action: 'password', snapshot: transition.snapshot }; + } + if (transition.action === 'email') { + return { action: 'email', snapshot: transition.snapshot }; + } return { action: 'recoverable', - result: await createStep6LoginTimeoutRecoverableResult( - 'login_timeout_error_page', - snapshot, - '提交密码后进入登录超时报错页。' - ), + result: transition.result, }; } @@ -2332,13 +2428,30 @@ async function waitForStep6PasswordSubmitTransition(passwordSubmittedAt, timeout }; } if (snapshot.state === 'login_timeout_error_page') { + const transition = await createStep6LoginTimeoutRecoveryTransition( + 'login_timeout_error_page', + snapshot, + '提交密码后进入登录超时报错页。', + { + loginVerificationRequestedAt: passwordSubmittedAt, + via: 'password_submit_timeout_recovered', + } + ); + if (transition.action === 'done') { + return { + action: 'done', + result: transition.result, + }; + } + if (transition.action === 'password') { + return { action: 'password', snapshot: transition.snapshot }; + } + if (transition.action === 'email') { + return { action: 'email', snapshot: transition.snapshot }; + } return { action: 'recoverable', - result: await createStep6LoginTimeoutRecoverableResult( - 'login_timeout_error_page', - snapshot, - '提交密码后进入登录超时报错页。' - ), + result: transition.result, }; } if (snapshot.state === 'oauth_consent_page') { @@ -2375,11 +2488,22 @@ async function waitForStep6SwitchTransition(loginVerificationRequestedAt, timeou } if (snapshot.state === 'login_timeout_error_page') { - return await createStep6LoginTimeoutRecoverableResult( + const transition = await createStep6LoginTimeoutRecoveryTransition( 'login_timeout_error_page', snapshot, - '切换到一次性验证码登录后进入登录超时报错页。' + '切换到一次性验证码登录后进入登录超时报错页。', + { + loginVerificationRequestedAt, + via: 'switch_to_one_time_code_timeout_recovered', + } ); + if (transition.action === 'done') { + return transition.result; + } + if (transition.action === 'password' || transition.action === 'email') { + return transition; + } + return transition.result; } if (snapshot.state === 'oauth_consent_page') { @@ -2401,11 +2525,22 @@ async function waitForStep6SwitchTransition(loginVerificationRequestedAt, timeou }); } if (snapshot.state === 'login_timeout_error_page') { - return await createStep6LoginTimeoutRecoverableResult( + const transition = await createStep6LoginTimeoutRecoveryTransition( 'login_timeout_error_page', snapshot, - '切换到一次性验证码登录后进入登录超时报错页。' + '切换到一次性验证码登录后进入登录超时报错页。', + { + loginVerificationRequestedAt, + via: 'switch_to_one_time_code_timeout_recovered', + } ); + if (transition.action === 'done') { + return transition.result; + } + if (transition.action === 'password' || transition.action === 'email') { + return transition; + } + return transition.result; } if (snapshot.state === 'oauth_consent_page') { throw new Error(`切换到一次性验证码登录后页面直接进入 OAuth 授权页,未经过登录验证码页。URL: ${snapshot.url}`); @@ -2419,7 +2554,7 @@ async function waitForStep6SwitchTransition(loginVerificationRequestedAt, timeou }); } -async function step6SwitchToOneTimeCodeLogin(snapshot) { +async function step6SwitchToOneTimeCodeLogin(payload, snapshot) { const switchTrigger = snapshot?.switchTrigger || findOneTimeCodeLoginTrigger(); if (!switchTrigger || !isActionEnabled(switchTrigger)) { return createStep6RecoverableResult('missing_one_time_code_trigger', normalizeStep6Snapshot(inspectLoginAuthState()), { @@ -2441,6 +2576,12 @@ async function step6SwitchToOneTimeCodeLogin(snapshot) { via: result.via || 'switch_to_one_time_code_login', }); } + if (result?.action === 'password') { + return step6LoginFromPasswordPage(payload, result.snapshot); + } + if (result?.action === 'email') { + return step6LoginFromEmailPage(payload, result.snapshot); + } return result; } @@ -2452,7 +2593,7 @@ async function step6LoginFromPasswordPage(payload, snapshot) { if (!hasPassword) { if (currentSnapshot.switchTrigger) { log('步骤 7:当前未提供密码,改走一次性验证码登录。', 'warn'); - return step6SwitchToOneTimeCodeLogin(currentSnapshot); + return step6SwitchToOneTimeCodeLogin(payload, currentSnapshot); } return createStep6RecoverableResult('missing_password_and_one_time_code_trigger', currentSnapshot, { @@ -2482,8 +2623,14 @@ async function step6LoginFromPasswordPage(payload, snapshot) { log(`步骤 7:${transition.result.message || '提交密码后仍未进入登录验证码页面,准备重新执行步骤 7。'}`, 'warn'); return transition.result; } + if (transition.action === 'password') { + return step6LoginFromPasswordPage(payload, transition.snapshot); + } + if (transition.action === 'email') { + return step6LoginFromEmailPage(payload, transition.snapshot); + } if (transition.action === 'switch') { - return step6SwitchToOneTimeCodeLogin(transition.snapshot); + return step6SwitchToOneTimeCodeLogin(payload, transition.snapshot); } return createStep6RecoverableResult('password_submit_unknown', normalizeStep6Snapshot(inspectLoginAuthState()), { @@ -2492,7 +2639,7 @@ async function step6LoginFromPasswordPage(payload, snapshot) { } if (currentSnapshot.switchTrigger) { - return step6SwitchToOneTimeCodeLogin(currentSnapshot); + return step6SwitchToOneTimeCodeLogin(payload, currentSnapshot); } return createStep6RecoverableResult('password_page_unactionable', currentSnapshot, { @@ -2532,6 +2679,9 @@ async function step6LoginFromEmailPage(payload, snapshot) { log(`步骤 7:${transition.result.message || '提交邮箱后仍未进入目标页面,准备重新执行步骤 7。'}`, 'warn'); return transition.result; } + if (transition.action === 'email') { + return step6LoginFromEmailPage(payload, transition.snapshot); + } if (transition.action === 'password') { return step6LoginFromPasswordPage(payload, transition.snapshot); } @@ -2545,11 +2695,10 @@ async function step6_login(payload) { const { email } = payload; if (!email) throw new Error('登录时缺少邮箱地址。'); - log(`步骤 7:正在使用 ${email} 登录...`); - const snapshot = normalizeStep6Snapshot(await waitForKnownLoginAuthState(15000)); if (snapshot.state === 'verification_page') { + log('步骤 7:认证页已在登录验证码页,开始确认页面是否稳定。'); return finalizeStep6VerificationReady({ logLabel: '步骤 7 收尾', loginVerificationRequestedAt: null, @@ -2558,19 +2707,39 @@ async function step6_login(payload) { } if (snapshot.state === 'login_timeout_error_page') { - log('步骤 7:检测到登录超时报错,准备重新执行步骤 7。', 'warn'); - return await createStep6LoginTimeoutRecoverableResult( + log('步骤 7:检测到登录超时报错页,先尝试恢复当前页面。', 'warn'); + const transition = await createStep6LoginTimeoutRecoveryTransition( 'login_timeout_error_page', snapshot, - '当前页面处于登录超时报错页。' + '当前页面处于登录超时报错页。', + { + loginVerificationRequestedAt: null, + via: 'login_timeout_initial_recovered', + } ); + if (transition.action === 'done') { + return finalizeStep6VerificationReady({ + logLabel: '步骤 7 收尾', + loginVerificationRequestedAt: transition.result.loginVerificationRequestedAt || null, + via: transition.result.via || 'login_timeout_initial_recovered', + }); + } + if (transition.action === 'email') { + return step6LoginFromEmailPage(payload, transition.snapshot); + } + if (transition.action === 'password') { + return step6LoginFromPasswordPage(payload, transition.snapshot); + } + return transition.result; } if (snapshot.state === 'email_page') { + log(`步骤 7:正在使用 ${email} 登录...`); return step6LoginFromEmailPage(payload, snapshot); } if (snapshot.state === 'password_page') { + log('步骤 7:认证页已在密码页,继续当前登录流程。'); return step6LoginFromPasswordPage(payload, snapshot); } diff --git a/manifest.json b/manifest.json index ced3774..08b28e8 100644 --- a/manifest.json +++ b/manifest.json @@ -1,8 +1,8 @@ { "manifest_version": 3, "name": "codex-oauth-automation-extension", - "version": "5.8", - "version_name": "Pro5.8", + "version": "7.0", + "version_name": "Pro7.0", "description": "用于自动执行多步骤 OAuth 注册流程", "permissions": [ "sidePanel", diff --git a/sidepanel/contribution-mode.js b/sidepanel/contribution-mode.js index 50aedf7..c54081c 100644 --- a/sidepanel/contribution-mode.js +++ b/sidepanel/contribution-mode.js @@ -24,6 +24,8 @@ dom.rowSub2ApiPassword, dom.rowSub2ApiGroup, dom.rowSub2ApiDefaultProxy, + dom.rowCodex2ApiUrl, + dom.rowCodex2ApiAdminKey, dom.rowCustomPassword, dom.rowAccountRunHistoryHelperBaseUrl, ].filter(Boolean); diff --git a/sidepanel/sidepanel.css b/sidepanel/sidepanel.css index f40f857..c96c47d 100644 --- a/sidepanel/sidepanel.css +++ b/sidepanel/sidepanel.css @@ -806,6 +806,17 @@ header { flex-shrink: 0; } +#row-codex2api-url .data-label, +#row-codex2api-admin-key .data-label { + width: 76px; +} + +#row-codex2api-url .data-label { + font-size: 10px; + letter-spacing: 0.02em; + text-transform: none; +} + .data-value { font-size: 13px; color: var(--text-muted); @@ -2393,6 +2404,19 @@ header { line-height: 1.55; color: var(--text-secondary); margin-bottom: 14px; + white-space: pre-line; +} + +.modal-message a, +.modal-alert a { + color: var(--blue); + text-decoration: underline; + text-underline-offset: 2px; +} + +.modal-message a:hover, +.modal-alert a:hover { + color: var(--cyan); } .modal-alert { diff --git a/sidepanel/sidepanel.html b/sidepanel/sidepanel.html index 835ae4b..966a58c 100644 --- a/sidepanel/sidepanel.html +++ b/sidepanel/sidepanel.html @@ -112,6 +112,7 @@ + +
账户密码
@@ -235,31 +246,6 @@
- - - - -
+