diff --git a/README.md b/README.md index 43468ac..1899a4c 100644 --- a/README.md +++ b/README.md @@ -110,6 +110,27 @@ - `2925` 旧的“只填前缀”使用方式已经不再推荐,应该改为填写完整基邮箱 - 如果你手动填写了与当前 `Gmail / 2925 provide` 基邮箱不匹配的完整邮箱,侧边栏会在保存或执行 Step 3 时拦截 +## 2026-04-23 更新补充:自定义邮箱池 + +本次版本新增 `自定义邮箱池` 生成方式,用于把一批已经准备好的邮箱按顺序分配给自动流程: + +- 在 `邮箱生成` 中选择 `自定义邮箱池` +- 在新出现的 `邮箱池` 文本框里按“每行一个邮箱”填写 +- `Auto` 运行次数会自动跟随邮箱池数量,无需再手动对齐轮数 +- 同一目标轮次的失败重试会继续复用当前轮邮箱,不会提前跳到下一个 +- 实际收码仍然走当前 `Mail` 对应的邮箱服务,因此应保证邮箱池里的地址与当前收码链路匹配 + +## 2026-04-23 更新补充:自定义邮箱服务号池 + +当 `Mail = 自定义邮箱` 时,现在也可以直接维护一组“自定义号池”: + +- 在 `邮箱服务` 选择 `自定义邮箱` +- 在新出现的 `自定义号池` 文本框里按“每行一个邮箱”填写 +- `Auto` 运行次数会自动跟随号池数量 +- 只要当前邮箱还没成功认证、也没出现手机号验证,就会持续复用这个邮箱重试 +- 只有成功认证,或明确出现 `add-phone / 手机号验证` 时,才会切换到号池中的下一个邮箱 +- 这条链路只负责分配注册邮箱;第 `4 / 8` 步仍然保持手动输入验证码,不会改成自动轮询邮箱 + ## 快速开始 如果你只是想先跑通一套最稳的组合,建议直接按下面三种方案之一配置。 @@ -118,9 +139,9 @@ 1. `CPA` 填你的管理面板 OAuth 页面地址 2. `Mail` 选择 `QQ Mail`、`163 Mail` 或 `163 VIP Mail` -3. `邮箱生成` 选择 `DuckDuckGo` 或 `Cloudflare` +3. `邮箱生成` 选择 `DuckDuckGo`、`Cloudflare` 或 `自定义邮箱池` 4. 若你选择 `Cloudflare`,先按下文把 Cloudflare Email Routing 配好 -5. 点击 `获取` 生成邮箱,或手动粘贴一个你能收信的邮箱 +5. 若你选择 `自定义邮箱池`,就在 `邮箱池` 中按行填入邮箱;否则点击 `获取` 生成邮箱,或手动粘贴一个你能收信的邮箱 6. 先单步验证 `Step 1 ~ Step 4` 7. 验证没问题后再点右上角 `Auto` @@ -132,7 +153,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 +169,7 @@ 4. 通过后再执行步骤或 `Auto` 5. 当前项目中,`Mail = Hotmail` 时会直接使用账号池里的邮箱作为注册邮箱,不再走 `Duck / Cloudflare` 自动生成 -### 方案 D:`2925 账号池` +### 方案 E:`2925 账号池` 1. `Mail` 选择 `2925` 2. 在 `2925 账号池` 中添加 `邮箱 / 密码` @@ -177,6 +206,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` 支持五种验证码来源: @@ -327,17 +370,20 @@ Step 3 使用的注册邮箱。 来源有两种: - 手动粘贴 -- 点击 `获取` 自动生成邮箱(DuckDuckGo 或 Cloudflare) +- 按当前生成方式自动生成或分配邮箱(DuckDuckGo / Cloudflare / 自定义邮箱池) 注意: - 若 `邮箱生成 = Cloudflare`,插件里只需要维护 `CF 域名` +- 若 `邮箱生成 = 自定义邮箱池`,需要在 `邮箱池` 文本框中按行维护邮箱列表 +- 若 `Mail = 自定义邮箱` 且你希望多轮自动跑不同邮箱,可直接在 `自定义号池` 文本框中按行维护邮箱列表 - `CF 域名` 支持保存多个,并通过下拉框切换当前要生成的域名 - Cloudflare 侧的转发规则、Catch-all、路由目标邮箱等,都需要你自己提前在 Cloudflare 后台配置好 - 当 `Mail = Hotmail` 时,这个输入框由账号池自动同步当前账号邮箱 - 当 `Mail = Hotmail` 时,Step 3 会直接使用 Hotmail 账号池里的邮箱;`Duck / Cloudflare` 不参与自动邮箱生成 +- 当 `Mail = 自定义邮箱` 且启用了 `自定义号池` 时,Auto 会先为当前轮分配一个邮箱;后续普通失败不会换号,只有成功或出现手机号验证才会切到下一个邮箱 - 若你准备走 `Cloudflare`,更推荐把 `Mail` 设为 `QQ / 163 / 163 VIP`;`Inbucket` 仅在它能真实接收外部邮件并完成 Cloudflare 验证时再使用 -- 当前 `Auto` 按钮只负责 DuckDuckGo 地址获取 +- `Auto` 会按当前“邮箱生成”配置自动获取或分配邮箱;若当前是 `自定义邮箱池`,则会按邮箱池顺序取用 - 如果你使用 Inbucket,它只是验证码收件箱,不会自动生成 Inbucket 地址 ### `邮箱生成 = Cloudflare` 时的配置 @@ -487,7 +533,7 @@ Cloudflare 模式下,插件不会再调用 Cloudflare API 创建路由。 1. Step 1 打开 `https://chatgpt.com/` 2. 根据 `Mail` 选择邮箱来源 3. 如果 `Mail = Hotmail`,会从账号池自动分配一个可用账号 -4. 如果不是 Hotmail,则按当前“邮箱生成”配置尝试自动获取邮箱(Duck / Cloudflare / iCloud 等) +4. 如果 `Mail = 自定义邮箱` 且配置了 `自定义号池`,会按号池顺序分配当前轮邮箱;否则如果不是 Hotmail,则按当前“邮箱生成”配置尝试自动获取或分配邮箱(Duck / Cloudflare / iCloud / 自定义邮箱池等) 5. Step 2 点击注册、填写邮箱,并按真实落地页进入密码页或直接进入邮箱验证码页 6. 如果自动获取失败,暂停并等待你在侧边栏填写邮箱后点击 `Continue` 7. 继续执行 Step 3 ~ Step 10 @@ -528,6 +574,7 @@ Cloudflare 模式下,插件不会再调用 Cloudflare API 创建路由。 - 使用自定义密码或自动生成密码 - 在密码页填写密码并提交注册表单 - 后台会在真正把 Step 3 记为完成前,再确认页面是否已经推进;如果此时出现认证页 `重试` 页面,或 `/email-verification` 上的 `405 / Route Error` 重试页,会先通过共享恢复逻辑最多自动点击 5 次 `重试` 尝试恢复,再继续后续链路 +- Step 3 收尾阶段如果页面切换导致旧内容脚本失联,后台单次消息等待不会再卡住超过当前收尾预算;若最终仍未恢复,则会输出中文的步骤级错误,而不是直接暴露底层英文通信超时 实际使用的密码会写入会话状态,并同步到侧边栏显示。 @@ -606,6 +653,7 @@ Step 8 默认要求当前认证页已经处于登录验证码页。 - 验证码链路失败后按有限次数回退到 Step 7 - 如果进入登录超时报错/重试页,包括 `/email-verification` 上的 `405 / Route Error` 登录重试页,会直接报错并回到 Step 7,不会在 Step 8 内部点击 `重试` - 如果重试页内容中出现 `max_check_attempts`,会直接完全停止整个流程,并复用现有确认弹窗提醒先等待 15 到 30 分钟或更换浏览器,确认按钮显示为“我知道了” +- 当 `Mail = 自定义邮箱` 时,Step 8 的手动确认弹窗会额外提供一个“出现手机号验证”按钮;点击后会直接按真实 `add-phone` fatal 错误处理,日志和自动切号行为与页面实际进入手机号验证时保持一致 与 Step 4 类似,但会使用稍微不同的关键词组合去找登录验证码邮件。 @@ -623,7 +671,7 @@ Step 8 默认要求当前认证页已经处于登录验证码页。 - 等待按钮可点击 - 获取按钮坐标 - 通过 Chrome `debugger` 的输入事件点击该按钮 -- 点击后会持续检查页面是否真正离开当前状态;如果出现认证页 `重试` 页面,会先通过共享恢复逻辑最多自动点击 5 次 `重试` 尝试恢复,再重新执行当前轮的“继续”点击 +- 点击后会持续检查页面是否真正离开当前状态;如果点击后出现认证页 `重试` 页面,则直接报错,不会在 Step 9 内部点击 `重试` - 同时监听 `chrome.webNavigation.onBeforeNavigate` - 一旦捕获本地回调地址,就把结果保存到 `Callback` @@ -639,9 +687,9 @@ Step 8 默认要求当前认证页已经处于登录验证码页。 - 步骤 10 会拒绝任何不是真实 `/auth/callback`,或缺少 `code` / `state` 的本地回调地址 - 成功后的清理只会针对 `/auth` 这一类真实回调标签页,不会再泛化清理任意 localhost 路径 -- 侧边栏可切换“本地 CPA”策略,默认是 `全部回调` -- 选择 `全部回调` 时,即使 CPA 部署在本地,也会执行步骤 10 -- 选择 `跳过第10步` 时,仅当本地 CPA 且步骤 9 已拿到回调地址时,才会直接跳过步骤 10 +- 侧边栏可切换“回调方式”,默认是 `服务器部署` +- 选择 `服务器部署` 时,即使 CPA 部署在本地,也会执行步骤 10 +- 选择 `本地部署` 时,仅当本地 CPA 且步骤 9 已拿到回调地址时,才会直接跳过步骤 10 回到 CPA 面板: diff --git a/background.js b/background.js index 04f98d8..81c9048 100644 --- a/background.js +++ b/background.js @@ -140,22 +140,26 @@ const ICLOUD_LOGIN_URLS = [ ]; const ICLOUD_PROVIDER = 'icloud'; const GMAIL_PROVIDER = 'gmail'; +const GMAIL_ALIAS_GENERATOR = 'gmail-alias'; const HOTMAIL_PROVIDER = 'hotmail-api'; const LUCKMAIL_PROVIDER = 'luckmail-api'; const CLOUDFLARE_TEMP_EMAIL_PROVIDER = 'cloudflare-temp-email'; const CLOUDFLARE_TEMP_EMAIL_GENERATOR = 'cloudflare-temp-email'; +const CUSTOM_EMAIL_POOL_GENERATOR = 'custom-pool'; const HOTMAIL_MAILBOXES = ['INBOX', 'Junk']; const STOP_ERROR_MESSAGE = '流程已被用户停止。'; const CLOUDFLARE_SECURITY_BLOCK_ERROR_PREFIX = 'CF_SECURITY_BLOCKED::'; const CLOUDFLARE_SECURITY_BLOCK_USER_MESSAGE = '您已触发Cloudflare 安全防护系统,已完全停止流程,请不要短时间内多次进行重新发送验证码,连续刷新、反复点击重试会加重风控;请先关闭页面等待 15-30 分钟,让系统的临时限制自动解除。或者更换浏览器'; +const BROWSER_SWITCH_REQUIRED_ERROR_PREFIX = 'BROWSER_SWITCH_REQUIRED::'; const HUMAN_STEP_DELAY_MIN = 700; const HUMAN_STEP_DELAY_MAX = 2200; const STEP6_MAX_ATTEMPTS = 3; const STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS = 8; -const OAUTH_FLOW_TIMEOUT_MS = 6 * 60 * 1000; +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'; @@ -257,6 +261,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, @@ -268,12 +274,16 @@ const PERSISTED_SETTING_DEFAULTS = { mail2925Mode: DEFAULT_MAIL_2925_MODE, mail2925UseAccountPool: false, emailGenerator: 'duck', + customMailProviderPool: [], + customEmailPool: [], autoDeleteUsedIcloudAlias: false, icloudHostPreference: 'auto', + icloudFetchMode: 'reuse_existing', accountRunHistoryTextEnabled: false, accountRunHistoryHelperBaseUrl: DEFAULT_ACCOUNT_RUN_HISTORY_HELPER_BASE_URL, gmailBaseEmail: '', mail2925BaseEmail: '', + currentMail2925AccountId: '', emailPrefix: '', inbucketHost: '', inbucketMailbox: '', @@ -286,6 +296,7 @@ const PERSISTED_SETTING_DEFAULTS = { cloudflareTempEmailAdminAuth: '', cloudflareTempEmailCustomAuth: '', cloudflareTempEmailReceiveMailbox: '', + cloudflareTempEmailUseRandomSubdomain: false, cloudflareTempEmailDomain: '', cloudflareTempEmailDomains: [], hotmailAccounts: [], @@ -336,6 +347,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: {}, // 各来源页面最近一次打开的地址记录。 @@ -650,9 +663,21 @@ function getAutoRunTimerStatusPayload(plan) { function normalizeEmailGenerator(value = '') { const normalized = String(value || '').trim().toLowerCase(); + const customEmailPoolGenerator = typeof CUSTOM_EMAIL_POOL_GENERATOR === 'string' + ? CUSTOM_EMAIL_POOL_GENERATOR + : 'custom-pool'; + const gmailAliasGenerator = typeof GMAIL_ALIAS_GENERATOR === 'string' + ? GMAIL_ALIAS_GENERATOR + : 'gmail-alias'; if (normalized === 'custom' || normalized === 'manual') { return 'custom'; } + if (normalized === gmailAliasGenerator) { + return gmailAliasGenerator; + } + if (normalized === customEmailPoolGenerator) { + return customEmailPoolGenerator; + } if (normalized === 'icloud') { return 'icloud'; } @@ -661,8 +686,60 @@ function normalizeEmailGenerator(value = '') { return 'duck'; } +function normalizeIcloudFetchMode(value = '') { + const normalized = String(value || '').trim().toLowerCase(); + return normalized === 'always_new' ? 'always_new' : 'reuse_existing'; +} + +function normalizeCustomEmailPool(value = []) { + const source = Array.isArray(value) + ? value + : String(value || '').split(/[\r\n,,;;]+/); + + return source + .map((item) => String(item || '').trim().toLowerCase()) + .filter((item) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(item)); +} + +function isCustomEmailPoolGenerator(stateOrValue = {}) { + const generator = typeof stateOrValue === 'string' + ? stateOrValue + : stateOrValue?.emailGenerator; + const customEmailPoolGenerator = typeof CUSTOM_EMAIL_POOL_GENERATOR === 'string' + ? CUSTOM_EMAIL_POOL_GENERATOR + : 'custom-pool'; + return normalizeEmailGenerator(generator) === customEmailPoolGenerator; +} + +function getCustomEmailPool(state = {}) { + return normalizeCustomEmailPool(state?.customEmailPool); +} + +function getCustomEmailPoolEmailForRun(state = {}, targetRun = 1) { + const entries = getCustomEmailPool(state); + const numericRun = Math.max(1, Math.floor(Number(targetRun) || 1)); + return entries[numericRun - 1] || ''; +} + +function getCustomMailProviderPool(state = {}) { + return normalizeCustomEmailPool(state?.customMailProviderPool); +} + +function getCustomMailProviderPoolEmailForRun(state = {}, targetRun = 1) { + const entries = getCustomMailProviderPool(state); + const numericRun = Math.max(1, Math.floor(Number(targetRun) || 1)); + return entries[numericRun - 1] || ''; +} + 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 = '') { @@ -838,6 +915,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), }; @@ -883,6 +961,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': @@ -904,15 +986,22 @@ function normalizePersistentSettingValue(key, value) { return Boolean(value); case 'emailGenerator': return normalizeEmailGenerator(value); + case 'customMailProviderPool': + case 'customEmailPool': + return normalizeCustomEmailPool(value); case 'autoDeleteUsedIcloudAlias': case 'accountRunHistoryTextEnabled': + case 'cloudflareTempEmailUseRandomSubdomain': return Boolean(value); case 'icloudHostPreference': return normalizeIcloudHost(value) || 'auto'; + case 'icloudFetchMode': + return normalizeIcloudFetchMode(value); case 'accountRunHistoryHelperBaseUrl': return normalizeAccountRunHistoryHelperBaseUrl(value); case 'gmailBaseEmail': case 'mail2925BaseEmail': + case 'currentMail2925AccountId': case 'emailPrefix': return String(value || '').trim(); case 'inbucketHost': @@ -2146,6 +2235,18 @@ function parseGmailBaseEmail(rawValue) { } function isGeneratedAliasProvider(stateOrProvider, mail2925Mode = undefined) { + if ( + stateOrProvider + && typeof stateOrProvider === 'object' + && !Array.isArray(stateOrProvider) + && normalizeEmailGenerator(stateOrProvider.emailGenerator) === ( + typeof CUSTOM_EMAIL_POOL_GENERATOR === 'string' + ? CUSTOM_EMAIL_POOL_GENERATOR + : 'custom-pool' + ) + ) { + return false; + } const provider = typeof stateOrProvider === 'string' ? stateOrProvider : stateOrProvider?.mailProvider; @@ -2303,6 +2404,18 @@ function getManagedAliasBaseEmail(state = {}, provider = state?.mailProvider) { } function isGeneratedAliasProvider(stateOrProvider, mail2925Mode = undefined) { + if ( + stateOrProvider + && typeof stateOrProvider === 'object' + && !Array.isArray(stateOrProvider) + && normalizeEmailGenerator(stateOrProvider.emailGenerator) === ( + typeof CUSTOM_EMAIL_POOL_GENERATOR === 'string' + ? CUSTOM_EMAIL_POOL_GENERATOR + : 'custom-pool' + ) + ) { + return false; + } const provider = typeof stateOrProvider === 'string' ? stateOrProvider : stateOrProvider?.mailProvider; @@ -3017,6 +3130,16 @@ async function pollLuckmailVerificationCode(step, state, pollPayload = {}) { excludeCodes: pollPayload.excludeCodes || [], }; + const initialCursor = normalizeLuckmailMailCursor((await getState()).currentLuckmailMailCursor); + if (!initialCursor.messageId && !initialCursor.receivedAt) { + const mailList = await client.user.getTokenMails(purchase.token); + const baselineCursor = buildLuckmailBaselineCursor(mailList?.mails || []); + await setLuckmailMailCursorState(baselineCursor); + if (baselineCursor?.messageId || baselineCursor?.receivedAt) { + await addLog(`步骤 ${step}:LuckMail 已保存当前邮箱旧邮件快照,后续仅使用新收到的验证码。`, 'info'); + } + } + let lastError = null; for (let attempt = 1; attempt <= maxAttempts; attempt++) { throwIfStopped(); @@ -3360,10 +3483,16 @@ async function validateIcloudSession(setupUrl) { return data; } -async function resolveIcloudPremiumMailService() { +async function resolveIcloudPremiumMailService(options = {}) { const errors = []; const state = await getState(); - const setupUrls = await getPreferredIcloudSetupUrls(state); + const explicitHost = normalizeIcloudHost(options?.hostPreference || options?.preferredHost || ''); + const setupUrls = explicitHost + ? (() => { + const forcedSetupUrl = getIcloudSetupUrlForHost(explicitHost); + return forcedSetupUrl ? [forcedSetupUrl] : []; + })() + : await getPreferredIcloudSetupUrls(state); for (const setupUrl of setupUrls) { try { @@ -3392,17 +3521,19 @@ function getIcloudAliasLabel() { return `MultiPage ${dateStr}`; } -async function checkIcloudSession() { - return withIcloudLoginHelp('检查 iCloud 会话', async () => { - const { setupUrl } = await resolveIcloudPremiumMailService(); +async function checkIcloudSession(options = {}) { + const actionLabel = String(options?.actionLabel || '检查 iCloud 会话').trim() || '检查 iCloud 会话'; + const { actionLabel: _ignoredActionLabel, ...resolveOptions } = options || {}; + return withIcloudLoginHelp(actionLabel, async () => { + const { setupUrl } = await resolveIcloudPremiumMailService(resolveOptions); await addLog(`iCloud:会话校验通过(${new URL(setupUrl).host})`, 'ok'); return { ok: true, setupUrl }; }); } -async function listIcloudAliases() { +async function listIcloudAliases(options = {}) { return withIcloudLoginHelp('加载 iCloud 隐私邮箱列表', async () => { - const { serviceUrl } = await resolveIcloudPremiumMailService(); + const { serviceUrl } = await resolveIcloudPremiumMailService(options); const response = await icloudRequest('GET', `${serviceUrl}/v2/hme/list`); const state = await getState(); return normalizeIcloudAliasList(response, { @@ -3492,9 +3623,10 @@ async function deleteUsedIcloudAliases() { return { deleted, skipped }; } -async function fetchIcloudHideMyEmail() { +async function fetchIcloudHideMyEmail(options = {}) { return withIcloudLoginHelp('获取 iCloud 隐私邮箱', async () => { throwIfStopped(); + const generateNew = Boolean(options?.generateNew); await addLog('iCloud:正在校验当前浏览器登录状态...', 'info'); const { serviceUrl, setupUrl } = await resolveIcloudPremiumMailService(); @@ -3507,12 +3639,16 @@ async function fetchIcloudHideMyEmail() { preservedEmails: getPreservedAliasMap(state), }); - const reusableAlias = pickReusableIcloudAlias(existingAliases); - if (reusableAlias) { - await setEmailState(reusableAlias.email); - await addLog(`iCloud:复用未使用别名 ${reusableAlias.email}`, 'ok'); - broadcastIcloudAliasesChanged({ reason: 'selected', email: reusableAlias.email }); - return reusableAlias.email; + if (!generateNew) { + const reusableAlias = pickReusableIcloudAlias(existingAliases); + if (reusableAlias) { + await setEmailState(reusableAlias.email); + await addLog(`iCloud:复用未使用别名 ${reusableAlias.email}`, 'ok'); + broadcastIcloudAliasesChanged({ reason: 'selected', email: reusableAlias.email }); + return reusableAlias.email; + } + } else { + await addLog('iCloud:已启用“始终创建新别名”,本次将跳过复用。', 'info'); } await addLog('iCloud:没有可复用别名,开始生成新的 Hide My Email 地址...', 'warn'); @@ -3637,11 +3773,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) { @@ -3649,7 +3805,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 = '') { @@ -3953,6 +4115,7 @@ function isRetryableContentScriptTransportError(error) { } const navigationUtils = self.MultiPageBackgroundNavigationUtils?.createNavigationUtils({ + DEFAULT_CODEX2API_URL, DEFAULT_SUB2API_URL, normalizeLocalCpaStep9Mode, }); @@ -4017,6 +4180,17 @@ function getTerminalSecurityBlockedTitle(error) { return 'Cloudflare 风控拦截'; } +function isBrowserSwitchRequiredError(error) { + return getErrorMessage(error).startsWith(BROWSER_SWITCH_REQUIRED_ERROR_PREFIX); +} + +function getBrowserSwitchRequiredMessage(error) { + const message = getErrorMessage(error); + return message.startsWith(BROWSER_SWITCH_REQUIRED_ERROR_PREFIX) + ? message.slice(BROWSER_SWITCH_REQUIRED_ERROR_PREFIX.length).trim() + : message; +} + function broadcastSecurityBlockedAlert(title = '流程已完全停止', message = CLOUDFLARE_SECURITY_BLOCK_USER_MESSAGE, alertText = '检测到 Cloudflare 风控,请暂停当前操作。') { chrome.runtime.sendMessage({ type: 'SECURITY_BLOCKED_ALERT', @@ -4040,6 +4214,13 @@ async function handleCloudflareSecurityBlocked(error) { return message; } +async function handleBrowserSwitchRequired(error) { + const message = getBrowserSwitchRequiredMessage(error) + || '检测到第 10 步的特殊冲突状态,请更换浏览器后重新进行注册登录。'; + await requestStop({ logMessage: message }); + return message; +} + function isVerificationMailPollingError(error) { if (typeof loggingStatus !== 'undefined' && loggingStatus?.isVerificationMailPollingError) { return loggingStatus.isVerificationMailPollingError(error); @@ -4128,6 +4309,8 @@ function getDownstreamStateResets(step) { sub2apiOAuthState: null, sub2apiGroupId: null, sub2apiDraftName: null, + codex2apiSessionId: null, + codex2apiOAuthState: null, flowStartTime: null, password: null, lastEmailTimestamp: null, @@ -4913,6 +5096,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); } @@ -4997,7 +5182,12 @@ async function handleStepData(step, payload) { }); } await finalizeIcloudAliasAfterSuccessfulFlow(latestState); - if (shouldUseCustomRegistrationEmail(latestState) && latestState.email) { + const shouldClearCustomPoolEmail = String(latestState?.emailGenerator || '').trim().toLowerCase() === ( + typeof CUSTOM_EMAIL_POOL_GENERATOR === 'string' + ? CUSTOM_EMAIL_POOL_GENERATOR + : 'custom-pool' + ); + if ((shouldUseCustomRegistrationEmail(latestState) || shouldClearCustomPoolEmail) && latestState.email) { await setEmailStateSilently(null); } break; @@ -5343,6 +5533,10 @@ async function executeStep(step, options = {}) { await handleCloudflareSecurityBlocked(err); throw new Error(STOP_ERROR_MESSAGE); } + if (isBrowserSwitchRequiredError(err)) { + await handleBrowserSwitchRequired(err); + throw new Error(STOP_ERROR_MESSAGE); + } if (!(deferRetryableTransportError && doesStepUseCompletionSignal(step) && isRetryableContentScriptTransportError(err))) { await setStepStatus(step, 'failed'); await addLog(`步骤 ${step} 失败:${err.message}`, 'error'); @@ -5407,9 +5601,21 @@ async function executeStepAndWait(step, delayAfter = 2000) { } function getEmailGeneratorLabel(generator) { + const customEmailPoolGenerator = typeof CUSTOM_EMAIL_POOL_GENERATOR === 'string' + ? CUSTOM_EMAIL_POOL_GENERATOR + : 'custom-pool'; + const gmailAliasGenerator = typeof GMAIL_ALIAS_GENERATOR === 'string' + ? GMAIL_ALIAS_GENERATOR + : 'gmail-alias'; if (generator === 'custom') { return '自定义邮箱'; } + if (generator === gmailAliasGenerator) { + return 'Gmail +tag 邮箱'; + } + if (generator === customEmailPoolGenerator) { + return '自定义邮箱池'; + } if (generator === 'icloud') { return 'iCloud 隐私邮箱'; } @@ -5440,6 +5646,7 @@ const mail2925SessionManager = self.MultiPageBackgroundMail2925Session?.createMa sleepWithStop, throwIfStopped, upsertMail2925AccountInList, + waitForTabComplete, waitForTabUrlMatch, }); @@ -5509,11 +5716,13 @@ const generatedEmailHelpers = self.MultiPageGeneratedEmailHelpers?.createGenerat buildGeneratedAliasEmail, buildCloudflareTempEmailHeaders, CLOUDFLARE_TEMP_EMAIL_GENERATOR, + CUSTOM_EMAIL_POOL_GENERATOR, DUCK_AUTOFILL_URL, fetch, fetchIcloudHideMyEmail, getCloudflareTempEmailAddressFromResponse, getCloudflareTempEmailConfig, + getCustomEmailPoolEmail: getCustomEmailPoolEmailForRun, getState, ensureMail2925AccountForFlow, joinCloudflareTempEmailUrl, @@ -5756,6 +5965,34 @@ async function ensureAutoEmailReady(targetRun, totalRuns, attemptRuns) { return currentState.email; } + if (isCustomMailProvider(currentState)) { + const poolSize = getCustomMailProviderPool(currentState).length; + if (poolSize > 0) { + const queuedEmail = getCustomMailProviderPoolEmailForRun(currentState, targetRun); + if (!queuedEmail) { + throw new Error(`自定义邮箱号池第 ${targetRun} 个邮箱不存在,请检查号池数量是否与自动轮数一致。`); + } + await setEmailState(queuedEmail); + await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮:自定义邮箱号池已就绪:${queuedEmail}(第 ${attemptRuns} 次尝试;第 4/8 步仍需手动输入验证码)===`, 'ok'); + return queuedEmail; + } + } + + if (isCustomEmailPoolGenerator(currentState)) { + const queuedEmail = getCustomEmailPoolEmailForRun(currentState, targetRun); + if (!queuedEmail) { + const poolSize = getCustomEmailPool(currentState).length; + throw new Error( + poolSize > 0 + ? `自定义邮箱池第 ${targetRun} 个邮箱不存在,请检查邮箱池数量是否与自动轮数一致。` + : '自定义邮箱池为空,请先至少填写 1 个邮箱。' + ); + } + await setEmailState(queuedEmail); + await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮:自定义邮箱池已就绪:${queuedEmail}(第 ${attemptRuns} 次尝试)===`, 'ok'); + return queuedEmail; + } + if (shouldUseCustomRegistrationEmail(currentState)) { await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮已暂停:请先填写自定义注册邮箱,然后继续 ===`, 'warn'); await broadcastAutoRunStatus('waiting_email', { @@ -5781,7 +6018,10 @@ async function ensureAutoEmailReady(targetRun, totalRuns, attemptRuns) { if (attempt > 1) { await addLog(`${generatorLabel}:正在进行第 ${attempt}/${EMAIL_FETCH_MAX_ATTEMPTS} 次自动获取重试...`, 'warn'); } - const generatedEmail = await fetchGeneratedEmail(currentState, { generateNew: true, generator }); + const generatedEmail = await fetchGeneratedEmail(currentState, { + generateNew: generator !== 'icloud' || normalizeIcloudFetchMode(currentState.icloudFetchMode) === 'always_new', + generator, + }); await addLog( `=== 目标 ${targetRun}/${totalRuns} 轮:${generatorLabel}已就绪:${generatedEmail}(第 ${attemptRuns} 次尝试,第 ${attempt}/${EMAIL_FETCH_MAX_ATTEMPTS} 次获取)===`, 'ok' @@ -5874,6 +6114,34 @@ async function ensureAutoEmailReady(targetRun, totalRuns, attemptRuns) { return currentState.email; } + if (isCustomMailProvider(currentState)) { + const poolSize = getCustomMailProviderPool(currentState).length; + if (poolSize > 0) { + const queuedEmail = getCustomMailProviderPoolEmailForRun(currentState, targetRun); + if (!queuedEmail) { + throw new Error(`自定义邮箱号池第 ${targetRun} 个邮箱不存在,请检查号池数量是否与自动轮数一致。`); + } + await setEmailState(queuedEmail); + await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮:自定义邮箱号池已就绪:${queuedEmail}(第 ${attemptRuns} 次尝试;第 4/8 步仍需手动输入验证码)===`, 'ok'); + return queuedEmail; + } + } + + if (isCustomEmailPoolGenerator(currentState)) { + const queuedEmail = getCustomEmailPoolEmailForRun(currentState, targetRun); + if (!queuedEmail) { + const poolSize = getCustomEmailPool(currentState).length; + throw new Error( + poolSize > 0 + ? `自定义邮箱池第 ${targetRun} 个邮箱不存在,请检查邮箱池数量是否与自动轮数一致。` + : '自定义邮箱池为空,请先至少填写 1 个邮箱。' + ); + } + await setEmailState(queuedEmail); + await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮:自定义邮箱池已就绪:${queuedEmail}(第 ${attemptRuns} 次尝试)===`, 'ok'); + return queuedEmail; + } + if (shouldUseCustomRegistrationEmail(currentState)) { await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮已暂停:请先填写自定义注册邮箱,然后继续 ===`, 'warn'); await broadcastAutoRunStatus('waiting_email', { @@ -5899,7 +6167,10 @@ async function ensureAutoEmailReady(targetRun, totalRuns, attemptRuns) { if (attempt > 1) { await addLog(`${generatorLabel}:正在进行第 ${attempt}/${EMAIL_FETCH_MAX_ATTEMPTS} 次自动获取重试...`, 'warn'); } - const generatedEmail = await fetchGeneratedEmail(currentState, { generateNew: true, generator }); + const generatedEmail = await fetchGeneratedEmail(currentState, { + generateNew: generator !== 'icloud' || normalizeIcloudFetchMode(currentState.icloudFetchMode) === 'always_new', + generator, + }); await addLog( `=== 目标 ${targetRun}/${totalRuns} 轮:${generatorLabel}已就绪:${generatedEmail}(第 ${attemptRuns} 次尝试,第 ${attempt}/${EMAIL_FETCH_MAX_ATTEMPTS} 次获取)===`, 'ok' @@ -5984,9 +6255,15 @@ async function runAutoSequenceFromStep(startStep, context = {}) { let restartFromStep1WithCurrentEmail = false; let step = Math.max(currentStartStep, 4); while (step <= LAST_STEP_ID) { + const latestState = await getState(); + const currentStatus = latestState.stepStatuses?.[step] || 'pending'; + if (isStepDoneStatus(currentStatus)) { + await addLog(`自动运行:步骤 ${step} 当前状态为 ${currentStatus},将直接继续后续流程。`, 'info'); + step += 1; + continue; + } try { await executeStepAndWait(step, AUTO_STEP_DELAYS[step]); - const latestState = await getState(); step += 1; } catch (err) { if (isStopError(err)) { @@ -6176,6 +6453,7 @@ const panelBridge = self.MultiPageBackgroundPanelBridge?.createPanelBridge({ closeConflictingTabsForSource, ensureContentScriptReadyOnTab, getPanelMode, + normalizeCodex2ApiUrl, normalizeSub2ApiUrl, rememberSourceLastUrl, sendToContentScript, @@ -6196,6 +6474,7 @@ const signupFlowHelpers = self.MultiPageSignupFlowHelpers?.createSignupFlowHelpe isGeneratedAliasProvider, isReusableGeneratedAliasEmail, isSignupEmailVerificationPageUrl, + isRetryableContentScriptTransportError, isHotmailProvider, isLuckmailProvider, isSignupPasswordPageUrl, @@ -6223,6 +6502,7 @@ const verificationFlowHelpers = self.MultiPageBackgroundVerificationFlow?.create getTabId, HOTMAIL_PROVIDER, isMail2925LimitReachedError, + isRetryableContentScriptTransportError, isStopError, LUCKMAIL_PROVIDER, MAIL_2925_VERIFICATION_INTERVAL_MS, @@ -6231,6 +6511,7 @@ const verificationFlowHelpers = self.MultiPageBackgroundVerificationFlow?.create pollHotmailVerificationCode, pollLuckmailVerificationCode, sendToContentScript, + sendToContentScriptResilient, sendToMailContentScriptResilient, setState, setStepStatus, @@ -6263,6 +6544,7 @@ const step2Executor = self.MultiPageBackgroundStep2?.createStep2Executor({ chrome, completeStepFromBackground, ensureContentScriptReadyOnTab, + ensureSignupAuthEntryPageReady, ensureSignupEntryPageReady, ensureSignupPostEmailPageReadyInTab, getTabId, @@ -6283,12 +6565,24 @@ const step3Executor = self.MultiPageBackgroundStep3?.createStep3Executor({ setState, SIGNUP_PAGE_INJECT_FILES, }); + +async function ensureIcloudMailSessionForVerification(options = {}) { + const flowState = options?.state || await getState().catch(() => ({})); + const hostPreference = getConfiguredIcloudHostPreference(flowState) + || normalizeIcloudHost(flowState?.preferredIcloudHost); + return checkIcloudSession({ + ...(hostPreference ? { hostPreference } : {}), + actionLabel: options?.actionLabel || '检查 iCloud 会话', + }); +} + const step4Executor = self.MultiPageBackgroundStep4?.createStep4Executor({ addLog, chrome, completeStepFromBackground, confirmCustomVerificationStepBypass: verificationFlowHelpers.confirmCustomVerificationStepBypass, ensureMail2925MailboxSession, + ensureIcloudMailSession: ensureIcloudMailSessionForVerification, getMailConfig, getTabId, HOTMAIL_PROVIDER, @@ -6335,6 +6629,7 @@ const step8Executor = self.MultiPageBackgroundStep8?.createStep8Executor({ CLOUDFLARE_TEMP_EMAIL_PROVIDER, confirmCustomVerificationStepBypass: verificationFlowHelpers.confirmCustomVerificationStepBypass, ensureMail2925MailboxSession, + ensureIcloudMailSession: ensureIcloudMailSessionForVerification, ensureStep8VerificationPageReady, getOAuthFlowRemainingMs, getOAuthFlowStepTimeoutMs, @@ -6366,6 +6661,7 @@ const step10Executor = self.MultiPageBackgroundStep10?.createStep10Executor({ getTabId, isLocalhostOAuthCallbackUrl, isTabAlive, + normalizeCodex2ApiUrl, normalizeSub2ApiUrl, rememberSourceLastUrl, reuseOrCreateTab, @@ -6432,6 +6728,7 @@ const messageRouter = self.MultiPageBackgroundMessageRouter?.createMessageRouter getPendingAutoRunTimerPlan, getSourceLabel, getState, + getTabId, getStopRequested: () => stopRequested, handleCloudflareSecurityBlocked, handleAutoRunLoopUnhandledError, @@ -6443,6 +6740,7 @@ const messageRouter = self.MultiPageBackgroundMessageRouter?.createMessageRouter isLocalhostOAuthCallbackUrl, isLuckmailProvider, isStopError, + isTabAlive, launchAutoRunTimerPlan, listIcloudAliases, listLuckmailPurchasesForManagement, @@ -6514,6 +6812,10 @@ async function ensureSignupEntryPageReady(step = 1) { return signupFlowHelpers.ensureSignupEntryPageReady(step); } +async function ensureSignupAuthEntryPageReady(step = 1) { + return signupFlowHelpers.ensureSignupEntryPageReady(step); +} + async function ensureSignupPasswordPageReadyInTab(tabId, step = 2, options = {}) { return signupFlowHelpers.ensureSignupPasswordPageReadyInTab(tabId, step, options); } @@ -6814,7 +7116,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'); @@ -6830,7 +7132,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); @@ -6868,7 +7170,7 @@ async function startOAuthFlowTimeoutWindow(options = {}) { oauthFlowDeadlineAt: deadlineAt, oauthFlowDeadlineSourceUrl: normalizeOAuthFlowSourceUrl(options.oauthUrl), }); - await addLog(`步骤 ${step}:已拿到新的 OAuth 登录地址,开始 6 分钟倒计时。`, 'info'); + await addLog(`步骤 ${step}:已拿到新的 OAuth 登录地址,开始 ${Math.round(OAUTH_FLOW_TIMEOUT_MS / 60000)} 分钟倒计时。`, 'info'); return deadlineAt; } @@ -7218,7 +7520,6 @@ async function getStep8PageState(tabId, responseTimeoutMs = 1500) { async function waitForStep8Ready(tabId, timeoutMs = STEP8_READY_WAIT_TIMEOUT_MS) { const start = Date.now(); let recovered = false; - let retryRecovered = false; while (Date.now() - start < timeoutMs) { throwIfStopped(); @@ -7229,7 +7530,6 @@ async function waitForStep8Ready(tabId, timeoutMs = STEP8_READY_WAIT_TIMEOUT_MS) if (pageState?.addPhonePage || pageState?.phoneVerificationPage) { await phoneVerificationHelpers.completePhoneVerificationFlow(tabId, pageState); recovered = false; - retryRecovered = false; await sleepWithStop(250); continue; } @@ -7237,20 +7537,9 @@ async function waitForStep8Ready(tabId, timeoutMs = STEP8_READY_WAIT_TIMEOUT_MS) throw new Error('步骤 9:认证页进入了手机号页面,当前不是 OAuth 同意页,无法继续自动授权。'); } if (pageState?.retryPage) { - await recoverAuthRetryPageOnTab(tabId, { - flow: 'auth', - logLabel: '步骤 9:检测到认证页重试页,正在点击“重试”恢复', - step: 8, - timeoutMs: Math.max(1000, Math.min(12000, timeoutMs)), - }); - retryRecovered = true; - await sleepWithStop(250); - continue; + throw new Error(`步骤 9:当前认证页已进入重试页,当前流程将直接报错。URL: ${pageState.url || 'unknown'}`); } if (pageState?.consentReady) { - if (retryRecovered) { - await addLog('步骤 9:认证页重试页已恢复,准备重新定位“继续”按钮...', 'info'); - } return pageState; } if (pageState === null && !recovered) { @@ -7415,18 +7704,7 @@ async function waitForStep8ClickEffect(tabId, baselineUrl, timeoutMs = STEP8_CLI throw new Error('步骤 9:点击“继续”后页面跳到了手机号页面,当前流程无法继续自动授权。'); } if (pageState?.retryPage) { - await recoverAuthRetryPageOnTab(tabId, { - flow: 'auth', - logLabel: '步骤 9:点击“继续”后进入重试页,正在点击“重试”恢复', - step: 8, - timeoutMs: Math.max(1000, Math.min(12000, timeoutMs)), - }); - return { - progressed: false, - reason: 'retry_page_recovered', - restartCurrentStep: true, - url: pageState.url || baselineUrl || '', - }; + throw new Error(`步骤 9:点击“继续”后页面进入认证页重试页,当前流程将直接报错。URL: ${pageState.url || baselineUrl || 'unknown'}`); } if (pageState === null) { if (!recovered) { @@ -7460,8 +7738,6 @@ function getStep8EffectLabel(effect) { switch (effect?.reason) { case 'url_changed': return `URL 已变化:${effect.url}`; - case 'retry_page_recovered': - return '页面进入重试页并已恢复,需要重新执行当前步骤'; case 'page_reloading': return '页面正在跳转或重载'; case 'left_consent_page': @@ -7578,7 +7854,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/auto-run-controller.js b/background/auto-run-controller.js index 3dee3e7..761a3e4 100644 --- a/background/auto-run-controller.js +++ b/background/auto-run-controller.js @@ -96,6 +96,12 @@ .join(';'); } + function shouldKeepCustomMailProviderPoolEmail(state = {}) { + return String(state?.mailProvider || '').trim().toLowerCase() === 'custom' + && Array.isArray(state?.customMailProviderPool) + && state.customMailProviderPool.length > 0; + } + async function logAutoRunFinalSummary(totalRuns, roundSummaries = []) { const summaries = buildAutoRunRoundSummaries(totalRuns, roundSummaries); const successRounds = summaries.filter((item) => item.status === 'success'); @@ -328,8 +334,10 @@ const resumingCurrentRound = continueCurrentOnFirstAttempt && targetRun === resumeCurrentRun; let attemptRun = resumingCurrentRound ? resumeAttemptRun : 1; let reuseExistingProgress = resumingCurrentRound; + const currentRoundState = await getState(); + const keepSameEmailUntilAddPhone = autoRunSkipFailures && shouldKeepCustomMailProviderPoolEmail(currentRoundState); const maxAttemptsForRound = autoRunSkipFailures - ? AUTO_RUN_MAX_RETRIES_PER_ROUND + 1 + ? (keepSameEmailUntilAddPhone ? Number.MAX_SAFE_INTEGER : AUTO_RUN_MAX_RETRIES_PER_ROUND + 1) : Math.max(1, attemptRun); while (attemptRun <= maxAttemptsForRound) { @@ -461,6 +469,7 @@ roundSummary.failureReasons.push(reason); const blockedByAddPhone = typeof isAddPhoneAuthFailure === 'function' && isAddPhoneAuthFailure(err); const blockedBySignupUserAlreadyExists = typeof isSignupUserAlreadyExistsFailure === 'function' + && !keepSameEmailUntilAddPhone && isSignupUserAlreadyExistsFailure(err); const canRetry = !blockedByAddPhone && !blockedBySignupUserAlreadyExists && autoRunSkipFailures && attemptRun < maxAttemptsForRound; @@ -555,7 +564,9 @@ }); forceFreshTabsNextRun = true; await addLog( - `自动重试:${Math.round(AUTO_RUN_RETRY_DELAY_MS / 1000)} 秒后开始第 ${targetRun}/${totalRuns} 轮第 ${attemptRun + 1} 次尝试(第 ${retryIndex}/${AUTO_RUN_MAX_RETRIES_PER_ROUND} 次重试)。`, + keepSameEmailUntilAddPhone + ? `自动重试:${Math.round(AUTO_RUN_RETRY_DELAY_MS / 1000)} 秒后继续使用当前邮箱,开始第 ${targetRun}/${totalRuns} 轮第 ${attemptRun + 1} 次尝试。` + : `自动重试:${Math.round(AUTO_RUN_RETRY_DELAY_MS / 1000)} 秒后开始第 ${targetRun}/${totalRuns} 轮第 ${attemptRun + 1} 次尝试(第 ${retryIndex}/${AUTO_RUN_MAX_RETRIES_PER_ROUND} 次重试)。`, 'warn' ); try { diff --git a/background/contribution-oauth.js b/background/contribution-oauth.js index 7dc5608..e95cbc3 100644 --- a/background/contribution-oauth.js +++ b/background/contribution-oauth.js @@ -599,6 +599,7 @@ body: { nickname: buildNickname(currentState, options.nickname), qq: buildContributionQq(currentState, options.qq), + email: normalizeString(currentState.email), source: 'cpa', channel: 'codex-extension', }, diff --git a/background/generated-email-helpers.js b/background/generated-email-helpers.js index 3d924a7..022d6af 100644 --- a/background/generated-email-helpers.js +++ b/background/generated-email-helpers.js @@ -7,11 +7,13 @@ buildGeneratedAliasEmail, buildCloudflareTempEmailHeaders, CLOUDFLARE_TEMP_EMAIL_GENERATOR, + CUSTOM_EMAIL_POOL_GENERATOR, DUCK_AUTOFILL_URL, fetch, fetchIcloudHideMyEmail, getCloudflareTempEmailAddressFromResponse, getCloudflareTempEmailConfig, + getCustomEmailPoolEmail, getState, ensureMail2925AccountForFlow, joinCloudflareTempEmailUrl, @@ -146,6 +148,7 @@ const requestedName = String(options.localPart || options.name || '').trim().toLowerCase() || generateCloudflareAliasLocalPart(); const payload = { enablePrefix: true, + enableRandomSubdomain: Boolean(config.useRandomSubdomain), name: requestedName, domain: config.domain, }; @@ -188,6 +191,24 @@ return result.email; } + async function fetchCustomEmailPoolEmail(state, options = {}) { + throwIfStopped(); + const latestState = state || await getState(); + const requestedIndex = Math.max(0, Math.floor(Number(options.poolIndex) || 0)); + const email = String(getCustomEmailPoolEmail?.(latestState, requestedIndex + 1) || '').trim().toLowerCase(); + if (!email) { + throw new Error( + requestedIndex > 0 + ? `自定义邮箱池第 ${requestedIndex + 1} 个邮箱不存在,请检查邮箱池配置。` + : '自定义邮箱池为空,请先至少填写 1 个邮箱。' + ); + } + + await setEmailState(email); + await addLog(`自定义邮箱池:已取用 ${email}`, 'ok'); + return email; + } + async function fetchManagedAliasEmail(state, options = {}) { throwIfStopped(); const provider = String(options.mailProvider || state?.mailProvider || '').trim().toLowerCase(); @@ -233,21 +254,45 @@ const mail2925Mode = options.mail2925Mode !== undefined ? options.mail2925Mode : currentState.mail2925Mode; - if (isGeneratedAliasProvider?.(provider, mail2925Mode)) { - return fetchManagedAliasEmail(currentState, options); - } const generator = normalizeEmailGenerator(options.generator ?? currentState.emailGenerator); + const mergedState = { + ...currentState, + mailProvider: provider || currentState.mailProvider, + mail2925Mode, + emailGenerator: generator, + }; + if (options.gmailBaseEmail !== undefined) { + mergedState.gmailBaseEmail = String(options.gmailBaseEmail || '').trim(); + } + if (options.mail2925BaseEmail !== undefined) { + mergedState.mail2925BaseEmail = String(options.mail2925BaseEmail || '').trim(); + } + if (options.customEmailPool !== undefined) { + mergedState.customEmailPool = options.customEmailPool; + } if (generator === 'custom') { throw new Error('当前邮箱生成方式为自定义邮箱,请直接填写注册邮箱。'); } + if (generator === CUSTOM_EMAIL_POOL_GENERATOR) { + return fetchCustomEmailPoolEmail(mergedState, options); + } + const shouldUseManagedAlias = typeof isGeneratedAliasProvider === 'function' + ? isGeneratedAliasProvider(mergedState, mail2925Mode) + : false; + if (shouldUseManagedAlias) { + return fetchManagedAliasEmail(mergedState, options); + } if (generator === 'icloud') { - return fetchIcloudHideMyEmail(); + const stateFetchMode = String(mergedState.icloudFetchMode || '').trim().toLowerCase(); + return fetchIcloudHideMyEmail({ + generateNew: Boolean(options.generateNew) || stateFetchMode === 'always_new', + }); } if (generator === 'cloudflare') { - return fetchCloudflareEmail(currentState, options); + return fetchCloudflareEmail(mergedState, options); } if (generator === CLOUDFLARE_TEMP_EMAIL_GENERATOR) { - return fetchCloudflareTempEmailAddress(currentState, options); + return fetchCloudflareTempEmailAddress(mergedState, options); } return fetchDuckEmail(options); } @@ -255,6 +300,7 @@ return { ensureCloudflareTempEmailConfig, fetchCloudflareEmail, + fetchCustomEmailPoolEmail, fetchCloudflareTempEmailAddress, fetchDuckEmail, fetchGeneratedEmail, diff --git a/background/mail-2925-session.js b/background/mail-2925-session.js index c78a350..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 }); @@ -168,6 +180,10 @@ } } + function normalizeMailboxEmail(value = '') { + return String(value || '').trim().toLowerCase(); + } + async function setCurrentMail2925Account(accountId, options = {}) { const { logMessage = '', updateLastUsedAt = false } = options; const state = await getState(); @@ -186,6 +202,7 @@ await syncMail2925Accounts(accounts.map((item) => (item.id === account.id ? nextAccount : item))); } + await setPersistentSettings({ currentMail2925AccountId: nextAccount.id }); await setState({ currentMail2925AccountId: nextAccount.id }); broadcastDataUpdate({ currentMail2925AccountId: nextAccount.id }); if (logMessage) { @@ -210,6 +227,7 @@ await syncMail2925Accounts(accounts.map((item) => (item.id === account.id ? nextAccount : item))); if (state.currentMail2925AccountId === account.id && nextAccount.enabled === false) { + await setPersistentSettings({ currentMail2925AccountId: '' }); await setState({ currentMail2925AccountId: null }); broadcastDataUpdate({ currentMail2925AccountId: null }); } @@ -224,6 +242,7 @@ await syncMail2925Accounts(nextAccounts); if (state.currentMail2925AccountId === accountId) { + await setPersistentSettings({ currentMail2925AccountId: '' }); await setState({ currentMail2925AccountId: null }); broadcastDataUpdate({ currentMail2925AccountId: null }); } @@ -239,6 +258,7 @@ await syncMail2925Accounts(nextAccounts); if (state.currentMail2925AccountId && !findMail2925Account(nextAccounts, state.currentMail2925AccountId)) { + await setPersistentSettings({ currentMail2925AccountId: '' }); await setState({ currentMail2925AccountId: null }); broadcastDataUpdate({ currentMail2925AccountId: null }); } @@ -391,16 +411,56 @@ 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, forceRelogin = false, actionLabel = '确保 2925 邮箱登录态', allowLoginWhenOnLoginPage = true, + expectedMailboxEmail = '', } = options; + const normalizedExpectedMailboxEmail = normalizeMailboxEmail(expectedMailboxEmail); + let account = null; - if (forceRelogin) { + if (forceRelogin || (allowLoginWhenOnLoginPage && normalizedExpectedMailboxEmail)) { account = await ensureMail2925AccountForFlow({ allowAllocate: true, preferredAccountId: accountId, @@ -477,16 +537,16 @@ } } - if (!forceRelogin && !isMail2925LoginUrl(openedUrl)) { + if (!forceRelogin && !isMail2925LoginUrl(openedUrl) && !normalizedExpectedMailboxEmail) { await addLog('2925:当前邮箱页未跳转到登录页,将直接复用已登录会话。', 'info'); return buildSuccessPayload(); } - if (!forceRelogin && !allowLoginWhenOnLoginPage) { + if (!forceRelogin && isMail2925LoginUrl(openedUrl) && !allowLoginWhenOnLoginPage) { await failMailboxSession(`2925:${actionLabel}失败,当前页面已跳转到登录页,且当前未启用 2925 账号池,不执行自动登录。`); } - if (!account) { + if (!account && (forceRelogin || allowLoginWhenOnLoginPage)) { account = await ensureMail2925AccountForFlow({ allowAllocate: true, preferredAccountId: accountId, @@ -509,35 +569,51 @@ } 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', step: 0, source: 'background', payload: { - email: account.email, - password: account.password, + email: account?.email || '', + password: account?.password || '', forceLogin: forceRelogin, + allowLoginWhenOnLoginPage, }, }, { - 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) { @@ -546,10 +622,44 @@ if (result?.limitReached) { throw new Error(`${MAIL2925_LIMIT_ERROR_PREFIX}${result.limitMessage || '子邮箱已达上限邮箱'}`); } + const actualMailboxEmail = normalizeMailboxEmail(result?.mailboxEmail || ''); + if (normalizedExpectedMailboxEmail && actualMailboxEmail && actualMailboxEmail !== normalizedExpectedMailboxEmail) { + if (allowLoginWhenOnLoginPage) { + await addLog( + `2925:当前邮箱页显示账号 ${actualMailboxEmail},与目标账号 ${normalizedExpectedMailboxEmail} 不一致,准备登出当前账号并登录目标账号。`, + 'warn' + ); + return ensureMail2925MailboxSession({ + accountId: account?.id || accountId || null, + forceRelogin: true, + allowLoginWhenOnLoginPage: true, + expectedMailboxEmail: normalizedExpectedMailboxEmail, + actionLabel, + }); + } + await failMailboxSession( + `2925:${actionLabel}失败,当前邮箱页显示账号 ${actualMailboxEmail},与目标账号 ${normalizedExpectedMailboxEmail} 不一致,且当前未启用 2925 账号池。` + ); + } + if (normalizedExpectedMailboxEmail && !actualMailboxEmail && result?.currentView === 'mailbox') { + await addLog('2925:未能识别当前邮箱页顶部邮箱地址,已跳过邮箱一致性校验。', 'warn'); + } if (!result?.loggedIn) { 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: '', @@ -613,6 +723,7 @@ }); if (!nextAccount) { + await setPersistentSettings({ currentMail2925AccountId: '' }); await setState({ currentMail2925AccountId: null }); broadcastDataUpdate({ currentMail2925AccountId: null }); if (typeof requestStop === 'function') { diff --git a/background/message-router.js b/background/message-router.js index b8580fd..15c503d 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); } @@ -130,6 +151,18 @@ if (payload.email) { await setEmailState(payload.email); } + if (payload.skipRegistrationFlow) { + const latestState = await getState(); + for (const skipStep of [3, 4, 5]) { + const status = latestState.stepStatuses?.[skipStep]; + if (status === 'running' || status === 'completed' || status === 'manual_completed') { + continue; + } + await setStepStatus(skipStep, 'skipped'); + } + await addLog('步骤 2:检测到当前已登录会话,已自动跳过步骤 3/4/5,流程将直接进入步骤 6。', 'warn'); + break; + } if (payload.skippedPasswordStep) { const latestState = await getState(); const step3Status = latestState.stepStatuses?.[3]; @@ -158,6 +191,14 @@ lastEmailTimestamp: payload.emailTimestamp || null, signupVerificationRequestedAt: null, }); + if (payload.skipProfileStep) { + const latestState = await getState(); + const step5Status = latestState.stepStatuses?.[5]; + if (step5Status !== 'running' && step5Status !== 'completed' && step5Status !== 'manual_completed') { + await setStepStatus(5, 'skipped'); + await addLog('步骤 4:检测到账号已直接进入已登录态,已自动跳过步骤 5。', 'warn'); + } + } break; case 8: await setState({ @@ -402,6 +443,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/signup-flow-helpers.js b/background/signup-flow-helpers.js index 3ba844b..dc64c9b 100644 --- a/background/signup-flow-helpers.js +++ b/background/signup-flow-helpers.js @@ -3,6 +3,7 @@ })(typeof self !== 'undefined' ? self : globalThis, function createSignupFlowHelpersModule() { function createSignupFlowHelpers(deps = {}) { const { + addLog, buildGeneratedAliasEmail, chrome, ensureContentScriptReadyOnTab, @@ -12,6 +13,7 @@ isGeneratedAliasProvider, isReusableGeneratedAliasEmail, isHotmailProvider, + isRetryableContentScriptTransportError = () => false, isLuckmailProvider, isSignupEmailVerificationPageUrl, isSignupPasswordPageUrl, @@ -164,20 +166,32 @@ logMessage: `步骤 ${step}:认证页仍在切换,正在等待页面恢复后继续确认提交流程...`, }); - const result = await sendToContentScriptResilient('signup-page', { - type: 'PREPARE_SIGNUP_VERIFICATION', - step, - source: 'background', - payload: { - password: password || '', - prepareSource: 'step3_finalize', - prepareLogLabel: '步骤 3 收尾', - }, - }, { - timeoutMs: 30000, - retryDelayMs: 700, - logMessage: `步骤 ${step}:密码已提交,正在确认是否进入下一页面,必要时自动恢复重试页...`, - }); + let result; + try { + result = await sendToContentScriptResilient('signup-page', { + type: 'PREPARE_SIGNUP_VERIFICATION', + step, + source: 'background', + payload: { + password: password || '', + prepareSource: 'step3_finalize', + prepareLogLabel: '步骤 3 收尾', + }, + }, { + timeoutMs: 30000, + retryDelayMs: 700, + logMessage: `步骤 ${step}:密码已提交,正在确认是否进入下一页面,必要时自动恢复重试页...`, + }); + } catch (error) { + if (isRetryableContentScriptTransportError(error)) { + const message = `步骤 ${step}:认证页在提交后切换过程中页面通信超时,未能重新就绪,暂时无法确认是否进入下一页面。请重试当前轮。`; + if (typeof addLog === 'function') { + await addLog(message, 'warn'); + } + throw new Error(message); + } + throw error; + } if (result?.error) { throw new Error(result.error); diff --git a/background/steps/confirm-oauth.js b/background/steps/confirm-oauth.js index 54a6075..59deab9 100644 --- a/background/steps/confirm-oauth.js +++ b/background/steps/confirm-oauth.js @@ -200,12 +200,6 @@ break; } - if (effect.restartCurrentStep) { - await addLog(`步骤 9:${getStep8EffectLabel(effect)},准备重新定位“继续”按钮并重试...`, 'warn'); - await sleepWithStop(STEP8_CLICK_RETRY_DELAY_MS); - continue; - } - if (round >= STEP8_MAX_ROUNDS) { throw new Error(`步骤 9:连续 ${STEP8_MAX_ROUNDS} 轮点击“继续”后页面仍无反应。`); } diff --git a/background/steps/fetch-login-code.js b/background/steps/fetch-login-code.js index 8bf53c4..a379a77 100644 --- a/background/steps/fetch-login-code.js +++ b/background/steps/fetch-login-code.js @@ -9,6 +9,8 @@ chrome, CLOUDFLARE_TEMP_EMAIL_PROVIDER, confirmCustomVerificationStepBypass, + ensureMail2925MailboxSession, + ensureIcloudMailSession, ensureStep8VerificationPageReady, getOAuthFlowRemainingMs, getOAuthFlowStepTimeoutMs, @@ -57,6 +59,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) { @@ -125,6 +141,15 @@ return; } + if (mail.source === 'icloud-mail' && typeof ensureIcloudMailSession === 'function') { + await addLog('步骤 8:正在确认 iCloud 邮箱登录态...', 'info'); + await ensureIcloudMailSession({ + state, + step: 8, + actionLabel: '步骤 8:确认 iCloud 邮箱登录态', + }); + } + throwIfStopped(); if ( mail.provider === HOTMAIL_PROVIDER @@ -134,7 +159,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 c1d9124..02f9aa5 100644 --- a/background/steps/fetch-signup-code.js +++ b/background/steps/fetch-signup-code.js @@ -10,6 +10,7 @@ completeStepFromBackground, confirmCustomVerificationStepBypass, ensureMail2925MailboxSession, + ensureIcloudMailSession, getMailConfig, getTabId, HOTMAIL_PROVIDER, @@ -24,6 +25,20 @@ throwIfStopped, } = deps; + 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) { @@ -58,7 +73,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 }); @@ -97,6 +112,15 @@ return; } + if (mail.source === 'icloud-mail' && typeof ensureIcloudMailSession === 'function') { + await addLog('步骤 4:正在确认 iCloud 邮箱登录态...', 'info'); + await ensureIcloudMailSession({ + state, + step: 4, + actionLabel: '步骤 4:确认 iCloud 邮箱登录态', + }); + } + throwIfStopped(); if ( mail.provider === HOTMAIL_PROVIDER @@ -111,6 +135,7 @@ accountId: state.currentMail2925AccountId || null, forceRelogin: false, allowLoginWhenOnLoginPage: Boolean(state?.mail2925UseAccountPool), + expectedMailboxEmail: getExpectedMail2925MailboxEmail(state), actionLabel: '步骤 4:确认 2925 邮箱登录态', }); } else { 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/open-chatgpt.js b/background/steps/open-chatgpt.js index 2476a9d..222454b 100644 --- a/background/steps/open-chatgpt.js +++ b/background/steps/open-chatgpt.js @@ -1,14 +1,142 @@ (function attachBackgroundStep1(root, factory) { root.MultiPageBackgroundStep1 = factory(); })(typeof self !== 'undefined' ? self : globalThis, function createBackgroundStep1Module() { + const STEP1_COOKIE_CLEAR_DOMAINS = [ + 'chatgpt.com', + 'chat.openai.com', + 'openai.com', + 'auth.openai.com', + 'auth0.openai.com', + 'accounts.openai.com', + ]; + const STEP1_COOKIE_CLEAR_ORIGINS = [ + 'https://chatgpt.com', + 'https://chat.openai.com', + 'https://auth.openai.com', + 'https://auth0.openai.com', + 'https://accounts.openai.com', + 'https://openai.com', + ]; + + function normalizeCookieDomainForStep1(domain) { + return String(domain || '').trim().replace(/^\.+/, '').toLowerCase(); + } + + function shouldClearStep1Cookie(cookie) { + const domain = normalizeCookieDomainForStep1(cookie?.domain); + if (!domain) return false; + return STEP1_COOKIE_CLEAR_DOMAINS.some((target) => ( + domain === target || domain.endsWith(`.${target}`) + )); + } + + function buildStep1CookieRemovalUrl(cookie) { + const host = normalizeCookieDomainForStep1(cookie?.domain); + const rawPath = String(cookie?.path || '/'); + const path = rawPath.startsWith('/') ? rawPath : `/${rawPath}`; + return `https://${host}${path}`; + } + + function getStep1ErrorMessage(error) { + return error?.message || String(error || '未知错误'); + } + + async function collectStep1Cookies(chromeApi) { + if (!chromeApi.cookies?.getAll) { + return []; + } + + const stores = chromeApi.cookies.getAllCookieStores + ? await chromeApi.cookies.getAllCookieStores() + : [{ id: undefined }]; + const cookies = []; + const seen = new Set(); + + for (const store of stores) { + const storeId = store?.id; + const batch = await chromeApi.cookies.getAll(storeId ? { storeId } : {}); + for (const cookie of batch || []) { + if (!shouldClearStep1Cookie(cookie)) continue; + const key = [ + cookie.storeId || storeId || '', + cookie.domain || '', + cookie.path || '', + cookie.name || '', + cookie.partitionKey ? JSON.stringify(cookie.partitionKey) : '', + ].join('|'); + if (seen.has(key)) continue; + seen.add(key); + cookies.push(cookie); + } + } + + return cookies; + } + + async function removeStep1Cookie(chromeApi, cookie) { + const details = { + url: buildStep1CookieRemovalUrl(cookie), + name: cookie.name, + }; + if (cookie.storeId) { + details.storeId = cookie.storeId; + } + if (cookie.partitionKey) { + details.partitionKey = cookie.partitionKey; + } + + try { + const result = await chromeApi.cookies.remove(details); + return Boolean(result); + } catch (error) { + console.warn('[MultiPage:step1] remove cookie failed', { + domain: cookie?.domain, + name: cookie?.name, + message: getStep1ErrorMessage(error), + }); + return false; + } + } + function createStep1Executor(deps = {}) { const { addLog, + chrome: chromeApi = globalThis.chrome, completeStepFromBackground, openSignupEntryTab, } = deps; + async function clearOpenAiCookiesBeforeStep1() { + if (!chromeApi?.cookies?.getAll || !chromeApi.cookies?.remove) { + await addLog('步骤 1:当前浏览器不支持 cookies API,跳过打开官网前 cookie 清理。', 'warn'); + return; + } + + await addLog('步骤 1:打开 ChatGPT 官网前清理 ChatGPT / OpenAI cookies...', 'info'); + const cookies = await collectStep1Cookies(chromeApi); + let removedCount = 0; + for (const cookie of cookies) { + if (await removeStep1Cookie(chromeApi, cookie)) { + removedCount += 1; + } + } + + if (chromeApi.browsingData?.removeCookies) { + try { + await chromeApi.browsingData.removeCookies({ + since: 0, + origins: STEP1_COOKIE_CLEAR_ORIGINS, + }); + } catch (error) { + await addLog(`步骤 1:browsingData 补扫 cookies 失败:${getStep1ErrorMessage(error)}`, 'warn'); + } + } + + await addLog(`步骤 1:已清理 ${removedCount} 个 ChatGPT / OpenAI cookies。`, 'ok'); + } + async function executeStep1() { + await clearOpenAiCookiesBeforeStep1(); await addLog('步骤 1:正在打开 ChatGPT 官网...'); await openSignupEntryTab(1); await completeStepFromBackground(1, {}); diff --git a/background/steps/platform-verify.js b/background/steps/platform-verify.js index 7edfe5d..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); } @@ -79,7 +159,8 @@ source: 'background', payload: { localhostUrl: state.localhostUrl, vpsPassword: state.vpsPassword }, }, { - timeoutMs: 30000, + timeoutMs: 125000, + responseTimeoutMs: 125000, retryDelayMs: 700, logMessage: '步骤 10:CPA 面板通信未就绪,正在等待页面恢复...', }); @@ -89,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。'); @@ -160,6 +283,7 @@ return { executeCpaStep10, + executeCodex2ApiStep10, executeStep10, executeSub2ApiStep10, }; diff --git a/background/steps/submit-signup-email.js b/background/steps/submit-signup-email.js index 77e6b65..b8cad06 100644 --- a/background/steps/submit-signup-email.js +++ b/background/steps/submit-signup-email.js @@ -7,6 +7,7 @@ chrome, completeStepFromBackground, ensureContentScriptReadyOnTab, + ensureSignupAuthEntryPageReady, ensureSignupEntryPageReady, ensureSignupPostEmailPageReadyInTab, getTabId, @@ -16,6 +17,107 @@ SIGNUP_PAGE_INJECT_FILES, } = deps; + function getErrorMessage(error) { + return String(typeof error === 'string' ? error : error?.message || ''); + } + + function isSignupEntryUnavailableErrorMessage(errorLike) { + const message = getErrorMessage(errorLike); + return /未找到可用的邮箱输入入口|当前页面没有可用的注册入口,也不在邮箱\/密码页/.test(message); + } + + function isRetryableStep2TransportErrorMessage(errorLike) { + const message = getErrorMessage(errorLike); + return /Content script on signup-page did not respond in \d+s|Receiving end does not exist|message channel closed|A listener indicated an asynchronous response|port closed before a response was received|did not respond in \d+s/i.test(message); + } + + function isLikelyLoggedInChatgptHomeUrl(rawUrl) { + const url = String(rawUrl || '').trim(); + if (!url) { + return false; + } + try { + const parsed = new URL(url); + const host = String(parsed.hostname || '').toLowerCase(); + if (!['chatgpt.com', 'www.chatgpt.com'].includes(host)) { + return false; + } + const path = String(parsed.pathname || ''); + if (/^\/(?:auth\/|create-account\/|email-verification|log-in|add-phone)(?:[/?#]|$)/i.test(path)) { + return false; + } + return true; + } catch { + return false; + } + } + + async function shouldForceAuthEntryRetry(tabId) { + if (!Number.isInteger(tabId) || typeof chrome?.tabs?.get !== 'function') { + return false; + } + try { + const tab = await chrome.tabs.get(tabId); + const currentUrl = String(tab?.url || ''); + return isLikelyLoggedInChatgptHomeUrl(currentUrl); + } catch { + return false; + } + } + + async function getTabUrl(tabId) { + if (!Number.isInteger(tabId) || typeof chrome?.tabs?.get !== 'function') { + return ''; + } + try { + const tab = await chrome.tabs.get(tabId); + return String(tab?.url || ''); + } catch { + return ''; + } + } + + async function completeStep2AsLoggedInSession(tabId, resolvedEmail, reasonMessage = '') { + const currentUrl = await getTabUrl(tabId); + if (!isLikelyLoggedInChatgptHomeUrl(currentUrl)) { + return false; + } + const reasonText = getErrorMessage(reasonMessage); + const reasonSuffix = reasonText ? `(触发原因:${reasonText})` : ''; + await addLog(`步骤 2:检测到当前会话已登录 ChatGPT,已跳过注册链路(步骤 3/4/5),将直接进入步骤 6。${reasonSuffix}`, 'warn'); + await completeStepFromBackground(2, { + email: resolvedEmail, + nextSignupState: 'already_logged_in_home', + nextSignupUrl: currentUrl || 'https://chatgpt.com/', + skippedPasswordStep: true, + skipRegistrationFlow: true, + }); + return true; + } + + async function submitSignupEmail(resolvedEmail, options = {}) { + const { + timeoutMs = 35000, + retryDelayMs = 700, + logMessage = '步骤 2:官网注册入口正在切换,等待页面恢复后继续输入邮箱...', + } = options; + + try { + return await sendToContentScriptResilient('signup-page', { + type: 'EXECUTE_STEP', + step: 2, + source: 'background', + payload: { email: resolvedEmail }, + }, { + timeoutMs, + retryDelayMs, + logMessage, + }); + } catch (error) { + return { error: getErrorMessage(error) }; + } + } + async function executeStep2(state) { const resolvedEmail = await resolveSignupEmailForFlow(state); @@ -34,19 +136,73 @@ }); } - const step2Result = await sendToContentScriptResilient('signup-page', { - type: 'EXECUTE_STEP', - step: 2, - source: 'background', - payload: { email: resolvedEmail }, - }, { - timeoutMs: 20000, + if (await shouldForceAuthEntryRetry(signupTabId)) { + await addLog('步骤 2:检测到当前位于已登录 ChatGPT 首页,先切换认证入口页再提交邮箱。', 'warn'); + try { + signupTabId = (await ensureSignupAuthEntryPageReady(2)).tabId; + } catch (entryError) { + const entryErrorMessage = getErrorMessage(entryError); + if (await completeStep2AsLoggedInSession(signupTabId, resolvedEmail, entryErrorMessage)) { + return; + } + await addLog('步骤 2:切换认证入口失败,正在重新打开官网入口并重试提交邮箱...', 'warn'); + signupTabId = (await ensureSignupEntryPageReady(2)).tabId; + } + } + + let step2Result = await submitSignupEmail(resolvedEmail, { + timeoutMs: 35000, retryDelayMs: 700, logMessage: '步骤 2:官网注册入口正在切换,等待页面恢复后继续输入邮箱...', }); if (step2Result?.error) { - throw new Error(step2Result.error); + const errorMessage = getErrorMessage(step2Result.error); + if (isSignupEntryUnavailableErrorMessage(errorMessage)) { + await addLog('步骤 2:未找到邮箱输入入口,正在切换认证入口页后重试一次...', 'warn'); + signupTabId = (await ensureSignupAuthEntryPageReady(2)).tabId; + step2Result = await submitSignupEmail(resolvedEmail, { + timeoutMs: 35000, + retryDelayMs: 700, + logMessage: '步骤 2:认证入口页已打开,正在重新提交邮箱...', + }); + + if (step2Result?.error) { + const retryErrorMessage = getErrorMessage(step2Result.error); + if (isSignupEntryUnavailableErrorMessage(retryErrorMessage)) { + if (await completeStep2AsLoggedInSession(signupTabId, resolvedEmail, retryErrorMessage)) { + return; + } + await addLog('步骤 2:认证入口仍不可用,正在重新进入官网注册入口再重试一次...', 'warn'); + signupTabId = (await ensureSignupEntryPageReady(2)).tabId; + step2Result = await submitSignupEmail(resolvedEmail, { + timeoutMs: 35000, + retryDelayMs: 700, + logMessage: '步骤 2:重试官网注册入口后正在重新提交邮箱...', + }); + } + } + } else if (isRetryableStep2TransportErrorMessage(errorMessage)) { + await addLog('步骤 2:注册入口页通信超时,正在切换认证入口页并重试提交邮箱...', 'warn'); + signupTabId = (await ensureSignupAuthEntryPageReady(2)).tabId; + step2Result = await submitSignupEmail(resolvedEmail, { + timeoutMs: 45000, + retryDelayMs: 700, + logMessage: '步骤 2:认证入口页已打开,正在重新提交邮箱...', + }); + } + } + + if (step2Result?.error) { + const finalErrorMessage = getErrorMessage(step2Result.error); + if ( + (isSignupEntryUnavailableErrorMessage(finalErrorMessage) + || isRetryableStep2TransportErrorMessage(finalErrorMessage)) + && await completeStep2AsLoggedInSession(signupTabId, resolvedEmail, finalErrorMessage) + ) { + return; + } + throw new Error(finalErrorMessage); } if (!step2Result?.alreadyOnPasswordPage) { diff --git a/background/tab-runtime.js b/background/tab-runtime.js index 54f6dfc..51619cb 100644 --- a/background/tab-runtime.js +++ b/background/tab-runtime.js @@ -376,6 +376,17 @@ return 30000; } + function resolveResponseTimeoutMs(message, requestedResponseTimeoutMs, remainingTimeoutMs = null) { + const fallbackTimeoutMs = getContentScriptResponseTimeoutMs(message); + const requestedTimeoutMs = Number.isFinite(Number(requestedResponseTimeoutMs)) + ? Math.max(1, Math.floor(Number(requestedResponseTimeoutMs))) + : fallbackTimeoutMs; + if (!Number.isFinite(Number(remainingTimeoutMs))) { + return requestedTimeoutMs; + } + return Math.max(1, Math.min(requestedTimeoutMs, Math.floor(Number(remainingTimeoutMs)))); + } + function getMessageDebugLabel(source, message, tabId = null) { const parts = [source || 'unknown', message?.type || 'UNKNOWN']; if (Number.isInteger(message?.step)) parts.push(`step=${message.step}`); @@ -439,7 +450,13 @@ pendingCommands.delete(source); reject(new Error(`Content script on ${source} did not respond in ${timeout / 1000}s. Try refreshing the tab and retry.`)); }, timeout); - pendingCommands.set(source, { message, resolve, reject, timer }); + pendingCommands.set(source, { + message, + resolve, + reject, + timer, + responseTimeoutMs: timeout, + }); console.log(LOG_PREFIX, `Command queued for ${source} (waiting for ready)`); }); } @@ -449,7 +466,7 @@ if (pending) { clearTimeout(pending.timer); pendingCommands.delete(source); - sendTabMessageWithTimeout(tabId, source, pending.message).then(pending.resolve).catch(pending.reject); + sendTabMessageWithTimeout(tabId, source, pending.message, pending.responseTimeoutMs).then(pending.resolve).catch(pending.reject); console.log(LOG_PREFIX, `Flushed queued command to ${source} (tab ${tabId})`); } } @@ -564,13 +581,13 @@ if (!entry || !entry.ready) { throwIfStopped(); - return queueCommand(source, message); + return queueCommand(source, message, responseTimeoutMs); } const alive = await isTabAlive(source); throwIfStopped(); if (!alive) { - return queueCommand(source, message); + return queueCommand(source, message, responseTimeoutMs); } throwIfStopped(); @@ -592,12 +609,18 @@ while (Date.now() - start < timeoutMs) { throwIfStopped(); attempt += 1; + const remainingTimeoutMs = Math.max(1, timeoutMs - (Date.now() - start)); + const effectiveResponseTimeoutMs = resolveResponseTimeoutMs( + message, + responseTimeoutMs, + remainingTimeoutMs + ); try { return await sendToContentScript( source, message, - responseTimeoutMs !== undefined ? { responseTimeoutMs } : {} + { responseTimeoutMs: effectiveResponseTimeoutMs } ); } catch (err) { const retryable = isRetryableContentScriptTransportError(err); @@ -631,12 +654,18 @@ while (Date.now() - start < timeoutMs) { throwIfStopped(); + const remainingTimeoutMs = Math.max(1, timeoutMs - (Date.now() - start)); + const effectiveResponseTimeoutMs = resolveResponseTimeoutMs( + message, + responseTimeoutMs, + remainingTimeoutMs + ); try { return await sendToContentScript( mail.source, message, - responseTimeoutMs !== undefined ? { responseTimeoutMs } : {} + { responseTimeoutMs: effectiveResponseTimeoutMs } ); } catch (err) { if (!isRetryableContentScriptTransportError(err)) { @@ -684,6 +713,7 @@ queueCommand, registerTab, rememberSourceLastUrl, + resolveResponseTimeoutMs, reuseOrCreateTab, sendTabMessageWithTimeout, sendToContentScript, diff --git a/background/verification-flow.js b/background/verification-flow.js index ff954b8..7e33c45 100644 --- a/background/verification-flow.js +++ b/background/verification-flow.js @@ -23,6 +23,7 @@ pollHotmailVerificationCode, pollLuckmailVerificationCode, sendToContentScript, + sendToContentScriptResilient, sendToMailContentScriptResilient, setState, sleepWithStop, @@ -30,6 +31,12 @@ VERIFICATION_POLL_MAX_ROUNDS, } = deps; + const isRetryableVerificationTransportError = typeof deps.isRetryableContentScriptTransportError === 'function' + ? deps.isRetryableContentScriptTransportError + : ((error) => /back\/forward cache|message channel is closed|Receiving end does not exist|port closed before a response was received|A listener indicated an asynchronous response|did not respond in \d+s/i.test( + String(typeof error === 'string' ? error : error?.message || '') + )); + function getVerificationCodeStateKey(step) { return step === 4 ? 'lastSignupCode' : 'lastLoginCode'; } @@ -38,6 +45,89 @@ return step === 4 ? '注册' : '登录'; } + function isLikelyLoggedInChatgptHomeUrl(rawUrl) { + const url = String(rawUrl || '').trim(); + if (!url) return false; + + try { + const parsed = new URL(url); + const host = String(parsed.hostname || '').toLowerCase(); + if (!['chatgpt.com', 'www.chatgpt.com'].includes(host)) { + return false; + } + const path = String(parsed.pathname || ''); + if (/^\/(?:auth\/|create-account\/|email-verification|log-in|add-phone)(?:[/?#]|$)/i.test(path)) { + return false; + } + return true; + } catch { + return false; + } + } + + function isSignupProfilePageUrl(rawUrl) { + const url = String(rawUrl || '').trim(); + if (!url) return false; + + try { + const parsed = new URL(url); + const host = String(parsed.hostname || '').toLowerCase(); + if (!['auth.openai.com', 'auth0.openai.com', 'accounts.openai.com'].includes(host)) { + return false; + } + return /\/create-account\/profile(?:[/?#]|$)/i.test(String(parsed.pathname || '')); + } catch { + return false; + } + } + + async function detectStep4PostSubmitFallback(tabId, options = {}) { + const timeoutMs = Math.max(1000, Number(options.timeoutMs) || 8000); + const pollIntervalMs = Math.max(100, Number(options.pollIntervalMs) || 250); + const startedAt = Date.now(); + let lastUrl = ''; + + while (Date.now() - startedAt < timeoutMs) { + throwIfStopped(); + try { + const tab = await chrome.tabs.get(tabId); + const currentUrl = String(tab?.url || '').trim(); + if (currentUrl) { + lastUrl = currentUrl; + } + + if (isLikelyLoggedInChatgptHomeUrl(currentUrl)) { + return { + success: true, + reason: 'chatgpt_home', + skipProfileStep: true, + url: currentUrl, + }; + } + + if (isSignupProfilePageUrl(currentUrl)) { + return { + success: true, + reason: 'signup_profile', + skipProfileStep: false, + url: currentUrl, + }; + } + } catch { + // Keep polling until timeout; tab may be mid-navigation. + } + + await sleepWithStop(pollIntervalMs); + } + + return { + success: false, + reason: 'unknown', + skipProfileStep: false, + url: lastUrl, + }; + } + function getVerificationResendStateKey() { return 'verificationResendCount'; } @@ -98,6 +188,9 @@ if (response?.error) { throw new Error(response.error); } + if (step === 8 && response?.addPhoneDetected) { + throw new Error('步骤 8:验证码提交后页面进入手机号页面,当前流程无法继续自动授权。 URL: https://auth.openai.com/add-phone'); + } if (!response?.confirmed) { throw new Error(`步骤 ${step}:已取消手动${verificationLabel}验证码确认。`); } @@ -445,6 +538,17 @@ `步骤 ${step}:距离下次重新发送验证码还差 ${Math.ceil(remainingBeforeResendMs / 1000)} 秒,继续刷新邮箱(第 ${round}/${maxRounds} 轮)...`, 'info' ); + const configuredIntervalMs = Math.max( + 1, + Number(payloadOverrides.intervalMs) + || Number(pollOverrides.intervalMs) + || 3000 + ); + const cooldownSleepMs = Math.min( + remainingBeforeResendMs, + Math.max(1000, Math.min(configuredIntervalMs, 3000)) + ); + await sleepWithStop(cooldownSleepMs); continue; } @@ -594,19 +698,54 @@ } await chrome.tabs.update(signupTabId, { active: true }); - const result = await sendToContentScript('signup-page', { + const baseResponseTimeoutMs = await getResponseTimeoutMsForStep( + step, + options, + step === 7 ? 45000 : 30000, + `填写${getVerificationCodeLabel(step)}验证码` + ); + const message = { type: 'FILL_CODE', step, source: 'background', payload: { code }, - }, { - responseTimeoutMs: await getResponseTimeoutMsForStep( - step, - options, - step === 7 ? 45000 : 30000, - `填写${getVerificationCodeLabel(step)}验证码` - ), - }); + }; + let result; + if (typeof sendToContentScriptResilient === 'function') { + try { + result = await sendToContentScriptResilient('signup-page', message, { + timeoutMs: Math.max(baseResponseTimeoutMs + 15000, 30000), + retryDelayMs: 700, + responseTimeoutMs: baseResponseTimeoutMs, + logMessage: `步骤 ${step}:认证页正在切换,等待页面重新就绪后继续确认验证码提交结果...`, + }); + } catch (err) { + if (step === 4 && isRetryableVerificationTransportError(err)) { + const fallback = await detectStep4PostSubmitFallback(signupTabId, { + timeoutMs: 9000, + pollIntervalMs: 300, + }); + if (fallback.success) { + const fallbackLabel = fallback.reason === 'chatgpt_home' + ? 'ChatGPT 已登录首页' + : '注册资料页'; + await addLog(`步骤 4:验证码提交后页面已切换到${fallbackLabel},按提交成功继续。`, 'warn'); + return { + success: true, + assumed: true, + transportRecovered: true, + skipProfileStep: Boolean(fallback.skipProfileStep), + url: fallback.url, + }; + } + } + throw err; + } + } else { + result = await sendToContentScript('signup-page', message, { + responseTimeoutMs: baseResponseTimeoutMs, + }); + } if (result && result.error) { throw new Error(result.error); @@ -732,6 +871,7 @@ `步骤 ${step}:提交失败后距离下次重新发送验证码还差 ${Math.ceil(remainingBeforeResendMs / 1000)} 秒,先继续刷新邮箱(${attempt + 1}/${maxSubmitAttempts})...`, 'warn' ); + await sleepWithStop(Math.min(remainingBeforeResendMs, 2000)); continue; } @@ -756,6 +896,7 @@ emailTimestamp: result.emailTimestamp, code: result.code, phoneVerificationRequired: Boolean(submitResult.addPhonePage), + ...(step === 4 && submitResult?.skipProfileStep ? { skipProfileStep: true } : {}), }); triggerPostSuccessMailboxCleanup(step, mail); return { diff --git a/content/activation-utils.js b/content/activation-utils.js index fbda8d9..c1ef6bf 100644 --- a/content/activation-utils.js +++ b/content/activation-utils.js @@ -47,7 +47,15 @@ return true; } - return /(?:认证失败|回调 URL 提交失败):\s*/i.test(text); + if (/请更新\s*cli\s*proxy\s*api\s*或检查连接/i.test(text)) { + return true; + } + + if (/bad request|state code error|failed to exchange authorization code for tokens|failed to save authentication tokens|unknown or expired state|invalid state|state is required|code or error is required|invalid redirect_url|provider does not match state|failed to persist oauth callback|timeout waiting for oauth callback|oauth flow timed out/i.test(text)) { + return true; + } + + return /(?:认证失败|回调\s*url\s*提交失败|回调url提交失败|提交回调失败)\s*[::]?\s*/i.test(text); } return { 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 0bbb977..4263f87 100644 --- a/content/mail-2925.js +++ b/content/mail-2925.js @@ -515,8 +515,9 @@ function detectMail2925ViewState() { return { view: 'limit', limitMessage }; } - if (findMailItems().length > 0) { - return { view: 'mailbox', limitMessage: '' }; + const mailboxEmail = getMail2925DisplayedMailboxEmail(); + if (findMailItems().length > 0 || mailboxEmail) { + return { view: 'mailbox', limitMessage: '', mailboxEmail }; } if (findMail2925LoginPasswordInput() && findMail2925LoginEmailInput()) { @@ -531,6 +532,67 @@ function detectMail2925ViewState() { return { view: 'unknown', limitMessage: '' }; } +function getMail2925DisplayedMailboxEmail() { + const directSelectors = [ + '.right-header', + '[class~="right-header"]', + '[class*="right-header"]', + '[class*="user"] [class*="mail"]', + '[class*="user"] [class*="email"]', + '[class*="account"] [class*="mail"]', + '[class*="account"] [class*="email"]', + '[class*="header"] [class*="mail"]', + '[class*="header"] [class*="email"]', + ]; + + for (const selector of directSelectors) { + const candidates = document.querySelectorAll(selector); + for (const candidate of candidates) { + if (!isVisibleNode(candidate) || isMailItemNode(candidate)) { + continue; + } + const email = extractEmails(candidate.textContent || candidate.innerText || '') + .find((value) => /@2925\.com$/i.test(String(value || '').trim())) || ''; + if (email) { + return email; + } + } + } + + const topCandidates = Array.from(document.querySelectorAll('body *')) + .filter((node) => { + if (!isVisibleNode(node) || isMailItemNode(node)) { + return false; + } + const rect = typeof node.getBoundingClientRect === 'function' + ? node.getBoundingClientRect() + : null; + if (!rect) return false; + return rect.top >= 0 && rect.top <= Math.max(window.innerHeight * 0.35, 280); + }) + .map((node) => { + const email = extractEmails(node.textContent || node.innerText || '') + .find((value) => /@2925\.com$/i.test(String(value || '').trim())) || ''; + return { node, email }; + }) + .filter((entry) => entry.email); + + if (!topCandidates.length) { + return ''; + } + + topCandidates.sort((left, right) => { + const leftRect = left.node.getBoundingClientRect(); + const rightRect = right.node.getBoundingClientRect(); + if (leftRect.top !== rightRect.top) { + return leftRect.top - rightRect.top; + } + return leftRect.left - rightRect.left; + }); + + return topCandidates[0]?.email || ''; +} + function isCheckboxChecked(node) { const checkbox = node?.matches?.('input[type="checkbox"], [role="checkbox"]') ? node @@ -873,6 +935,7 @@ async function ensureMail2925Session(payload = {}) { const email = String(payload?.email || '').trim(); const password = String(payload?.password || ''); const forceLogin = Boolean(payload?.forceLogin); + const allowLoginWhenOnLoginPage = payload?.allowLoginWhenOnLoginPage !== false; log(`步骤 0:2925 登录态检查开始,当前地址 ${location.href},forceLogin=${forceLogin ? 'true' : 'false'}`, 'info'); for (let attempt = 0; attempt < 10; attempt += 1) { @@ -893,9 +956,19 @@ async function ensureMail2925Session(payload = {}) { ok: true, loggedIn: true, currentView: 'mailbox', + mailboxEmail: currentState.mailboxEmail || '', }; } if (currentState.view === 'login') { + if (!forceLogin && !allowLoginWhenOnLoginPage) { + return { + ok: false, + loggedIn: false, + currentView: 'login', + requiresLogin: true, + mailboxEmail: '', + }; + } break; } await sleep(500); @@ -908,6 +981,7 @@ async function ensureMail2925Session(payload = {}) { ok: true, loggedIn: true, currentView: 'mailbox', + mailboxEmail: loginState.mailboxEmail || '', }; } if (loginState.view === 'limit') { @@ -919,6 +993,15 @@ async function ensureMail2925Session(payload = {}) { limitMessage: loginState.limitMessage, }; } + if (!forceLogin && !allowLoginWhenOnLoginPage && loginState.view === 'login') { + return { + ok: false, + loggedIn: false, + currentView: 'login', + requiresLogin: true, + mailboxEmail: '', + }; + } const emailInput = findMail2925LoginEmailInput(); const passwordInput = findMail2925LoginPasswordInput(); @@ -951,6 +1034,7 @@ async function ensureMail2925Session(payload = {}) { loggedIn: true, currentView: 'mailbox', usedCredentials: true, + mailboxEmail: finalState.mailboxEmail || getMail2925DisplayedMailboxEmail() || '', }; } diff --git a/content/signup-page.js b/content/signup-page.js index f0cae4a..fa867c7 100644 --- a/content/signup-page.js +++ b/content/signup-page.js @@ -397,13 +397,82 @@ function getSignupEntryStateSummary(snapshot = inspectSignupEntryState()) { } function getSignupEntryDiagnostics() { + const view = typeof window !== 'undefined' ? window : globalThis; + const safeGetComputedStyle = (el) => { + if (!el || typeof view?.getComputedStyle !== 'function') { + return null; + } + try { + return view.getComputedStyle(el); + } catch { + return null; + } + }; + const buildRectSummary = (el) => { + const rect = typeof el?.getBoundingClientRect === 'function' + ? el.getBoundingClientRect() + : null; + return rect + ? { + width: Math.round(rect.width || 0), + height: Math.round(rect.height || 0), + } + : null; + }; + const buildVisibilityMeta = (el) => { + const style = safeGetComputedStyle(el); + return { + className: String(el?.className || '').slice(0, 200), + hidden: Boolean(el?.hidden), + ariaHidden: el?.getAttribute?.('aria-hidden') || '', + inert: typeof el?.hasAttribute === 'function' ? el.hasAttribute('inert') : false, + display: style?.display || '', + visibility: style?.visibility || '', + opacity: style?.opacity || '', + pointerEvents: style?.pointerEvents || '', + }; + }; + const findBlockingAncestor = (el) => { + let current = el?.parentElement || null; + while (current) { + const style = safeGetComputedStyle(current); + const rect = buildRectSummary(current); + const hidden = Boolean(current.hidden); + const ariaHidden = current.getAttribute?.('aria-hidden') || ''; + const inert = typeof current.hasAttribute === 'function' ? current.hasAttribute('inert') : false; + const blockedByStyle = Boolean( + style + && ( + style.display === 'none' + || style.visibility === 'hidden' + || style.opacity === '0' + || style.pointerEvents === 'none' + ) + ); + const blockedByRect = Boolean(rect && (rect.width === 0 || rect.height === 0)); + if (hidden || ariaHidden === 'true' || inert || blockedByStyle || blockedByRect) { + return { + tag: (current.tagName || '').toLowerCase(), + id: current.id || '', + className: String(current.className || '').slice(0, 200), + hidden, + ariaHidden, + inert, + display: style?.display || '', + visibility: style?.visibility || '', + opacity: style?.opacity || '', + pointerEvents: style?.pointerEvents || '', + rect, + }; + } + current = current.parentElement; + } + return null; + }; const actionCandidates = document.querySelectorAll( 'a, button, [role="button"], [role="link"], input[type="button"], input[type="submit"]' ); const allActions = Array.from(actionCandidates).map((el) => { - const rect = typeof el?.getBoundingClientRect === 'function' - ? el.getBoundingClientRect() - : null; const text = getActionText(el); return { tag: (el.tagName || '').toLowerCase(), @@ -411,12 +480,7 @@ function getSignupEntryDiagnostics() { text: text.slice(0, 80), visible: isVisibleElement(el), enabled: isActionEnabled(el), - rect: rect - ? { - width: Math.round(rect.width || 0), - height: Math.round(rect.height || 0), - } - : null, + rect: buildRectSummary(el), }; }); const visibleActions = Array.from(actionCandidates) @@ -429,7 +493,20 @@ function getSignupEntryDiagnostics() { enabled: isActionEnabled(el), })) .filter((item) => item.text); - const signupLikeActions = allActions + const signupLikeActions = Array.from(actionCandidates) + .map((el) => { + const text = getActionText(el); + return { + tag: (el.tagName || '').toLowerCase(), + type: el.getAttribute?.('type') || '', + text: text.slice(0, 80), + visible: isVisibleElement(el), + enabled: isActionEnabled(el), + rect: buildRectSummary(el), + ...buildVisibilityMeta(el), + blockingAncestor: findBlockingAncestor(el), + }; + }) .filter((item) => item.text && SIGNUP_ENTRY_TRIGGER_PATTERN.test(item.text)) .slice(0, 12); @@ -437,15 +514,133 @@ function getSignupEntryDiagnostics() { url: location.href, title: document.title || '', readyState: document.readyState || '', + viewport: { + innerWidth: Math.round(Number(view?.innerWidth) || 0), + innerHeight: Math.round(Number(view?.innerHeight) || 0), + outerWidth: Math.round(Number(view?.outerWidth) || 0), + outerHeight: Math.round(Number(view?.outerHeight) || 0), + devicePixelRatio: Number(view?.devicePixelRatio) || 0, + }, hasEmailInput: Boolean(getSignupEmailInput()), hasPasswordInput: Boolean(getSignupPasswordInput()), bodyContainsSignupText: SIGNUP_ENTRY_TRIGGER_PATTERN.test(getPageTextSnapshot()), + signupLikeActionCounts: { + total: signupLikeActions.length, + visible: signupLikeActions.filter((item) => item.visible).length, + hidden: signupLikeActions.filter((item) => !item.visible).length, + }, signupLikeActions, visibleActions, bodyTextPreview: getPageTextSnapshot().slice(0, 240), }; } +function getSignupPasswordDiagnostics() { + const view = typeof window !== 'undefined' ? window : globalThis; + const safeGetComputedStyle = (el) => { + if (!el || typeof view?.getComputedStyle !== 'function') { + return null; + } + try { + return view.getComputedStyle(el); + } catch { + return null; + } + }; + const buildRectSummary = (el) => { + const rect = typeof el?.getBoundingClientRect === 'function' + ? el.getBoundingClientRect() + : null; + return rect + ? { + width: Math.round(rect.width || 0), + height: Math.round(rect.height || 0), + } + : null; + }; + const buildInputSummary = (el) => { + const style = safeGetComputedStyle(el); + return { + tag: (el?.tagName || '').toLowerCase(), + type: el?.getAttribute?.('type') || el?.type || '', + name: el?.getAttribute?.('name') || el?.name || '', + id: el?.id || '', + autocomplete: el?.getAttribute?.('autocomplete') || '', + placeholder: String(el?.getAttribute?.('placeholder') || '').slice(0, 80), + visible: isVisibleElement(el), + enabled: isActionEnabled(el), + valueLength: String(el?.value || '').length, + rect: buildRectSummary(el), + className: String(el?.className || '').slice(0, 200), + display: style?.display || '', + visibility: style?.visibility || '', + opacity: style?.opacity || '', + pointerEvents: style?.pointerEvents || '', + formAction: el?.form?.action || '', + }; + }; + const buildActionSummary = (el) => { + const style = safeGetComputedStyle(el); + return { + tag: (el?.tagName || '').toLowerCase(), + type: el?.getAttribute?.('type') || el?.type || '', + role: el?.getAttribute?.('role') || '', + text: getActionText(el).slice(0, 120), + visible: isVisibleElement(el), + enabled: isActionEnabled(el), + rect: buildRectSummary(el), + className: String(el?.className || '').slice(0, 200), + display: style?.display || '', + visibility: style?.visibility || '', + opacity: style?.opacity || '', + pointerEvents: style?.pointerEvents || '', + dataDdActionName: el?.getAttribute?.('data-dd-action-name') || '', + formAction: el?.form?.action || '', + }; + }; + const passwordInputs = Array.from(document.querySelectorAll( + 'input[type="password"], input[name*="password" i], input[autocomplete="new-password"], input[autocomplete="current-password"]' + )) + .map(buildInputSummary) + .slice(0, 8); + const actionCandidates = Array.from(document.querySelectorAll( + 'button, a, [role="button"], [role="link"], input[type="button"], input[type="submit"]' + )) + .map(buildActionSummary) + .filter((item) => item.text) + .slice(0, 16); + const visibleActions = actionCandidates.filter((item) => item.visible).slice(0, 12); + const submitButton = getSignupPasswordSubmitButton({ allowDisabled: true }); + const oneTimeCodeTrigger = findOneTimeCodeLoginTrigger(); + const retryState = getSignupPasswordTimeoutErrorPageState(); + + return { + url: location.href, + title: document.title || '', + readyState: document.readyState || '', + displayedEmail: getSignupPasswordDisplayedEmail(), + hasVisiblePasswordInput: Boolean(getSignupPasswordInput()), + passwordInputCount: passwordInputs.length, + visiblePasswordInputCount: passwordInputs.filter((item) => item.visible).length, + passwordInputs, + submitButton: submitButton ? buildActionSummary(submitButton) : null, + oneTimeCodeTrigger: oneTimeCodeTrigger ? buildActionSummary(oneTimeCodeTrigger) : null, + retryPage: Boolean(retryState), + retryEnabled: Boolean(retryState?.retryEnabled), + userAlreadyExistsBlocked: Boolean(retryState?.userAlreadyExistsBlocked), + visibleActions, + bodyTextPreview: getPageTextSnapshot().slice(0, 240), + }; +} + +function logSignupPasswordDiagnostics(context, level = 'warn') { + try { + log(`${context}:密码页诊断快照:${JSON.stringify(getSignupPasswordDiagnostics())}`, level); + } catch (error) { + console.warn('[MultiPage:signup-page] failed to build signup password diagnostics:', error?.message || error); + } +} + async function waitForSignupEntryState(options = {}) { const { timeout = 15000, @@ -632,6 +827,10 @@ async function step3_fillEmailPassword(payload) { snapshot = inspectSignupEntryState(); } + if (snapshot.state !== 'password_page' || !snapshot.passwordInput) { + logSignupPasswordDiagnostics('步骤 3:未能识别可填写的密码输入框'); + } + if (snapshot.state !== 'password_page' || !snapshot.passwordInput) { throw new Error('在密码页未找到密码输入框。URL: ' + location.href); } @@ -647,6 +846,12 @@ async function step3_fillEmailPassword(payload) { || getSignupPasswordSubmitButton({ allowDisabled: true }) || await waitForElementByText('button', /continue|sign\s*up|submit|注册|创建|create/i, 5000).catch(() => null); + if (!submitBtn) { + logSignupPasswordDiagnostics('步骤 3:未找到可提交的密码页按钮'); + } else if (typeof findOneTimeCodeLoginTrigger === 'function' && findOneTimeCodeLoginTrigger()) { + logSignupPasswordDiagnostics('步骤 3:当前密码页同时存在一次性验证码入口', 'info'); + } + // Report complete BEFORE submit, because submit causes page navigation // which kills the content script connection const signupVerificationRequestedAt = submitBtn ? Date.now() : null; @@ -1501,8 +1706,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({ @@ -1511,8 +1721,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 || ''))) { @@ -1522,7 +1733,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, }); } @@ -1725,6 +1975,7 @@ async function prepareSignupVerificationFlow(payload = {}, timeout = 30000) { const start = Date.now(); let recoveryRound = 0; const maxRecoveryRounds = 3; + let passwordPageDiagnosticsLogged = false; while (Date.now() - start < timeout && recoveryRound < maxRecoveryRounds) { throwIfStopped(); @@ -1763,6 +2014,10 @@ async function prepareSignupVerificationFlow(payload = {}, timeout = 30000) { } if (snapshot.state === 'password') { + if (!passwordPageDiagnosticsLogged) { + passwordPageDiagnosticsLogged = true; + logSignupPasswordDiagnostics(`${prepareLogLabel}:页面仍停留在密码页`); + } if (!password) { throw new Error('当前回到了密码页,但没有可用密码,无法自动重新提交。'); } @@ -1952,6 +2207,20 @@ async function fillVerificationCode(step, payload) { const { code } = payload; if (!code) throw new Error('未提供验证码。'); + if (step === 4 && isStep5Ready()) { + log(`步骤 ${step}:检测到页面已进入下一阶段,本次验证码提交按成功处理。`, 'ok'); + return { success: true, assumed: true, alreadyAdvanced: true }; + } + if (step === 8) { + if (isStep8Ready()) { + log(`步骤 ${step}:检测到页面已进入 OAuth 同意页,本次验证码提交按成功处理。`, 'ok'); + return { success: true, assumed: true, alreadyAdvanced: true }; + } + if (isAddPhonePageReady()) { + return { success: true, addPhonePage: true, url: location.href }; + } + } + log(`步骤 ${step}:正在填写验证码:${code}`); if (step === 8) { @@ -2097,13 +2366,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, }; } @@ -2132,13 +2418,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') { @@ -2175,13 +2478,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, }; } @@ -2207,13 +2527,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') { @@ -2250,11 +2587,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') { @@ -2276,11 +2624,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}`); @@ -2294,7 +2653,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()), { @@ -2316,6 +2675,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; } @@ -2327,7 +2692,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, { @@ -2357,8 +2722,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()), { @@ -2367,7 +2738,7 @@ async function step6LoginFromPasswordPage(payload, snapshot) { } if (currentSnapshot.switchTrigger) { - return step6SwitchToOneTimeCodeLogin(currentSnapshot); + return step6SwitchToOneTimeCodeLogin(payload, currentSnapshot); } return createStep6RecoverableResult('password_page_unactionable', currentSnapshot, { @@ -2407,6 +2778,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); } @@ -2420,11 +2794,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, @@ -2433,19 +2806,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/content/vps-panel.js b/content/vps-panel.js index 4e84d49..63af31c 100644 --- a/content/vps-panel.js +++ b/content/vps-panel.js @@ -19,7 +19,10 @@ //