diff --git a/README.md b/README.md index 523fb4c..085b1a7 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ 一个用于批量跑通 ChatGPT OAuth 注册/登录流程的 Chrome 扩展。 -当前版本基于侧边栏控制,支持单步执行、整套自动执行、停止当前流程、保存常用配置,以及通过 DuckDuckGo / QQ / 163 / 163 VIP / 126 / Inbucket / Hotmail 协助获取验证码。 +当前版本基于侧边栏控制,支持单步执行、整套自动执行、停止当前流程、保存常用配置,以及通过 DuckDuckGo / QQ / 163 / 163 VIP / 126 / Inbucket / Hotmail / Cloud Mail 协助获取验证码。 ## 插件效果 @@ -40,6 +40,7 @@ - 自动获取注册验证码与登录验证码 - 支持 `Hotmail`:继续使用 `邮箱 + 客户端 ID + 刷新令牌(refresh token)`,并可在远程服务与本地助手两种模式间切换 - 支持 `2925`:新增多账号池、自动登录登出、Step 4 / Step 8 命中“子邮箱已达上限邮箱”后的 24 小时禁用与自动切号 +- 支持 `Cloud Mail`:可通过 skymail.ink API 生成自定义域邮箱,也可作为转发收件通道轮询验证码 - 支持 `QQ Mail`、`163 Mail`、`163 VIP Mail`、`126 Mail`、`Inbucket mailbox` - 支持从 DuckDuckGo Email Protection 自动生成新的 `@duck.com` 地址 - 支持基于 Cloudflare 自定义域名自动生成随机邮箱前缀 @@ -208,10 +209,11 @@ Step 1 和 Step 10 都依赖这个地址。 ### `Mail` -支持七种验证码来源: +支持八种验证码来源: - `Hotmail` - `2925` +- `Cloud Mail` - `163 Mail` - `163 VIP Mail` - `126 Mail` @@ -222,6 +224,7 @@ Step 1 和 Step 10 都依赖这个地址。 - `Hotmail` 通过侧边栏里的 Hotmail 账号池选择账号,可切换为远程服务模式或本地助手模式 - `2925` 通过侧边栏里的 2925 账号池选择账号,并在 Step 4 / Step 8 前自动校验网页邮箱登录态 +- `Cloud Mail` 通过侧边栏配置 API 地址、管理员账号、接收邮箱或生成域名,可直接生成邮箱或轮询转发收件箱 - `QQ`、`163`、`163 VIP`、`126` 用于直接轮询网页邮箱 - `Inbucket` 通过你在侧边栏里配置的 host 访问 `mailbox` 页面:`https:///m//` @@ -521,7 +524,7 @@ Cloudflare 模式下,插件不会再调用 Cloudflare API 创建路由。 1. Step 1 打开 `https://chatgpt.com/` 2. 根据 `Mail` 选择邮箱来源 3. 如果 `Mail = Hotmail`,会从账号池自动分配一个可用账号 -4. 如果 `Mail = 自定义邮箱` 且配置了 `自定义号池`,会按号池顺序分配当前轮邮箱;否则如果不是 Hotmail,则按当前“邮箱生成”配置尝试自动获取或分配邮箱(Duck / Cloudflare / iCloud / 自定义邮箱池等) +4. 如果 `Mail = 自定义邮箱` 且配置了 `自定义号池`,会按号池顺序分配当前轮邮箱;否则如果不是 Hotmail,则按当前“邮箱生成”配置尝试自动获取或分配邮箱(Duck / Cloudflare / Cloud Mail / iCloud / 自定义邮箱池等) 5. Step 2 点击注册、填写邮箱,并按真实落地页进入密码页或直接进入邮箱验证码页 6. 如果自动获取失败,暂停并等待你在侧边栏填写邮箱后点击 `Continue` 7. 继续执行 Step 3 ~ Step 10 diff --git a/background.js b/background.js index d8b0884..c994566 100644 --- a/background.js +++ b/background.js @@ -49,6 +49,7 @@ importScripts( 'luckmail-utils.js', 'cloudflare-temp-email-utils.js', 'cloudmail-utils.js', + 'background/cloudmail-provider.js', 'icloud-utils.js', 'mail-provider-utils.js', 'content/activation-utils.js' @@ -251,7 +252,7 @@ const STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS = 8; 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_SUB2API_URL = ''; const DEFAULT_CODEX2API_URL = 'http://localhost:8080/admin/accounts'; const DEFAULT_GPC_HELPER_API_URL = 'https://gpc.qlhazycoder.top'; const DEFAULT_SUB2API_GROUP_NAME = 'codex'; @@ -2141,41 +2142,33 @@ function resolveCloudflareTempEmailPollTargetEmail(state = {}, pollPayload = {}, return normalizeCloudflareTempEmailReceiveMailbox(state.email); } -function getCloudMailConfig(state = {}) { - return { - baseUrl: normalizeCloudMailBaseUrl(state.cloudMailBaseUrl), - adminEmail: String(state.cloudMailAdminEmail || '').trim(), - adminPassword: String(state.cloudMailAdminPassword || ''), - token: String(state.cloudMailToken || '').trim(), - receiveMailbox: normalizeCloudMailReceiveMailbox(state.cloudMailReceiveMailbox), - domain: normalizeCloudMailDomain(state.cloudMailDomain), - domains: normalizeCloudMailDomains(state.cloudMailDomains), - }; -} - -function normalizeCloudMailReceiveMailbox(value = '') { - const normalized = normalizeCloudMailAddress(value); - if (!normalized) return ''; - return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(normalized) ? normalized : ''; -} - -function resolveCloudMailPollTargetEmail(state = {}, pollPayload = {}, config = getCloudMailConfig(state)) { - const configuredReceiveMailbox = normalizeCloudMailReceiveMailbox(config.receiveMailbox); - const mailProvider = String(state?.mailProvider || '').trim().toLowerCase(); - const emailGenerator = String(state?.emailGenerator || '').trim().toLowerCase(); - const shouldPreferConfiguredReceiveMailbox = mailProvider === CLOUD_MAIL_PROVIDER - && emailGenerator !== CLOUD_MAIL_GENERATOR; - if (shouldPreferConfiguredReceiveMailbox && configuredReceiveMailbox) { - return configuredReceiveMailbox; - } - - const requestedTarget = normalizeCloudMailReceiveMailbox(pollPayload.targetEmail); - if (requestedTarget) { - return requestedTarget; - } - - return normalizeCloudMailReceiveMailbox(state.email); -} +const cloudMailProvider = self.MultiPageBackgroundCloudMailProvider.createCloudMailProvider({ + addLog, + buildCloudMailHeaders, + CLOUD_MAIL_DEFAULT_PAGE_SIZE, + CLOUD_MAIL_GENERATOR, + CLOUD_MAIL_PROVIDER, + getCloudMailTokenFromResponse, + getState, + joinCloudMailUrl, + normalizeCloudMailAddress, + normalizeCloudMailBaseUrl, + normalizeCloudMailDomain, + normalizeCloudMailDomains, + normalizeCloudMailMailApiMessages, + pickVerificationMessageWithTimeFallback, + setEmailState, + setPersistentSettings, + sleepWithStop, + throwIfStopped, +}); +const { + getCloudMailConfig, + normalizeCloudMailReceiveMailbox, + fetchCloudMailAddress, + pollCloudMailVerificationCode, + resolveCloudMailPollTargetEmail, +} = cloudMailProvider; function normalizeSub2ApiGroupNames(value = '') { const source = Array.isArray(value) @@ -2364,15 +2357,23 @@ function normalizePersistentSettingValue(key, value) { ); case 'gopayHelperApiUrl': { - const legacyGpcHelperApiUrl = 'https://gpc.leftcode.xyz'; const defaultGpcHelperApiUrl = PERSISTED_SETTING_DEFAULTS.gopayHelperApiUrl || (typeof DEFAULT_GPC_HELPER_API_URL !== 'undefined' ? DEFAULT_GPC_HELPER_API_URL : 'https://gpc.qlhazycoder.top'); const normalizedGpcHelperApiUrl = self.GoPayUtils?.normalizeGpcHelperBaseUrl ? self.GoPayUtils.normalizeGpcHelperBaseUrl(value || defaultGpcHelperApiUrl) : String(value || defaultGpcHelperApiUrl).trim().replace(/\/+$/g, ''); - return normalizedGpcHelperApiUrl === legacyGpcHelperApiUrl - ? defaultGpcHelperApiUrl - : normalizedGpcHelperApiUrl; + if (!self.GoPayUtils?.normalizeGpcHelperBaseUrl) { + try { + const parsed = new URL(normalizedGpcHelperApiUrl); + const hostname = parsed.hostname.toLowerCase(); + if (hostname !== 'gpc.qlhazycoder.top' && hostname !== 'localhost' && hostname !== '127.0.0.1') { + return defaultGpcHelperApiUrl; + } + } catch { + return defaultGpcHelperApiUrl; + } + } + return normalizedGpcHelperApiUrl; } case 'gopayHelperApiKey': case 'gopayHelperCardKey': @@ -5378,252 +5379,6 @@ async function pollCloudflareTempEmailVerificationCode(step, state, pollPayload throw lastError || new Error(`步骤 ${step}:未在 Cloudflare Temp Email 中找到新的匹配验证码。`); } -// ============================================================ -// Cloud Mail (skymail.ink) Integration -// ============================================================ - -function ensureCloudMailConfig(state, options = {}) { - const { requireToken = false, requireCredentials = false, requireDomain = false } = options; - const config = getCloudMailConfig(state); - if (!config.baseUrl) { - throw new Error('Cloud Mail 服务地址为空或格式无效。'); - } - if (requireCredentials && (!config.adminEmail || !config.adminPassword)) { - throw new Error('Cloud Mail 缺少管理员邮箱或密码。'); - } - if (requireToken && !config.token) { - throw new Error('Cloud Mail 尚未获取到身份令牌,请先生成 Token。'); - } - if (requireDomain && !config.domain) { - throw new Error('Cloud Mail 域名为空或格式无效。'); - } - return config; -} - -async function requestCloudMailJson(config, path, options = {}) { - const { - method = 'POST', - payload, - timeoutMs = 20000, - requireToken = true, - } = options; - const url = joinCloudMailUrl(config.baseUrl, path); - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(new Error('timeout')), timeoutMs); - let response; - try { - response = await fetch(url, { - method, - headers: buildCloudMailHeaders(config, { - json: payload !== undefined, - token: requireToken ? undefined : '', - }), - body: payload !== undefined ? JSON.stringify(payload) : undefined, - signal: controller.signal, - }); - } catch (err) { - const errorMessage = err?.name === 'AbortError' - ? `Cloud Mail 请求超时(>${Math.round(timeoutMs / 1000)} 秒)` - : `Cloud Mail 请求失败:${err.message}`; - throw new Error(errorMessage); - } finally { - clearTimeout(timeoutId); - } - const text = await response.text(); - let parsed; - try { - parsed = text ? JSON.parse(text) : {}; - } catch { - parsed = text; - } - if (!response.ok) { - const payloadError = typeof parsed === 'object' && parsed - ? (parsed.message || parsed.error || parsed.msg) - : ''; - throw new Error(`Cloud Mail 请求失败:${payloadError || text || `HTTP ${response.status}`}`); - } - if (parsed && typeof parsed === 'object' && 'code' in parsed && Number(parsed.code) !== 200) { - throw new Error(`Cloud Mail 业务错误:${parsed.message || parsed.msg || `code=${parsed.code}`}`); - } - return parsed; -} - -async function ensureCloudMailToken(state, options = {}) { - const { forceRefresh = false } = options; - const latestState = state || await getState(); - const config = ensureCloudMailConfig(latestState, { requireCredentials: true }); - if (!forceRefresh && config.token) { - return { config, token: config.token }; - } - const loginConfig = { ...config, token: '' }; - const result = await requestCloudMailJson(loginConfig, '/api/public/genToken', { - method: 'POST', - payload: { email: config.adminEmail, password: config.adminPassword }, - requireToken: false, - }); - const token = getCloudMailTokenFromResponse(result); - if (!token) { - throw new Error('Cloud Mail 未返回可用 Token。'); - } - await setPersistentSettings({ cloudMailToken: token }); - return { config: { ...config, token }, token }; -} - -function generateCloudMailAliasLocalPart() { - const letters = 'abcdefghijklmnopqrstuvwxyz'; - const digits = '0123456789'; - const chars = []; - for (let i = 0; i < 6; i++) chars.push(letters[Math.floor(Math.random() * letters.length)]); - for (let i = 0; i < 4; i++) chars.push(digits[Math.floor(Math.random() * digits.length)]); - for (let i = chars.length - 1; i > 0; i--) { - const j = Math.floor(Math.random() * (i + 1)); - [chars[i], chars[j]] = [chars[j], chars[i]]; - } - return chars.join(''); -} - -async function fetchCloudMailAddress(state, options = {}) { - throwIfStopped(); - const latestState = state || await getState(); - const { config } = await ensureCloudMailToken(latestState); - const ensuredConfig = ensureCloudMailConfig({ ...latestState, cloudMailToken: config.token }, { - requireToken: true, - requireDomain: true, - }); - const requestedLocal = String(options.localPart || options.name || '').trim().toLowerCase() - || generateCloudMailAliasLocalPart(); - const address = `${requestedLocal}@${ensuredConfig.domain}`.toLowerCase(); - const payload = { list: [{ email: address }] }; - try { - await requestCloudMailJson(ensuredConfig, '/api/public/addUser', { method: 'POST', payload }); - } catch (err) { - if (/token|unauthor|401/i.test(String(err?.message || ''))) { - const refreshed = await ensureCloudMailToken(latestState, { forceRefresh: true }); - await requestCloudMailJson(refreshed.config, '/api/public/addUser', { method: 'POST', payload }); - } else { - throw err; - } - } - await setEmailState(address); - await addLog(`Cloud Mail:已生成 ${address}`, 'ok'); - return address; -} - -function summarizeCloudMailMessagesForLog(messages) { - return (messages || []) - .slice() - .sort((left, right) => { - const leftTime = Date.parse(left.receivedDateTime || '') || 0; - const rightTime = Date.parse(right.receivedDateTime || '') || 0; - return rightTime - leftTime; - }) - .slice(0, 3) - .map((message) => { - const receivedAt = message?.receivedDateTime || '未知时间'; - const sender = message?.from?.emailAddress?.address || '未知发件人'; - const subject = message?.subject || '(无主题)'; - const preview = String(message?.bodyPreview || '').replace(/\s+/g, ' ').trim().slice(0, 80); - const address = message?.address || '未知地址'; - return `[${address}] ${receivedAt} | ${sender} | ${subject} | ${preview}`; - }) - .join(' || '); -} - -async function listCloudMailMessages(state, options = {}) { - const latestState = state || await getState(); - const { config } = await ensureCloudMailToken(latestState); - const address = normalizeCloudMailAddress(options.address); - const pageSize = Number(options.limit) || CLOUD_MAIL_DEFAULT_PAGE_SIZE; - const pageNum = Number(options.page) || 1; - const request = async (currentConfig) => requestCloudMailJson(currentConfig, '/api/public/emailList', { - method: 'POST', - payload: { - toEmail: address || undefined, - type: 0, - isDel: 0, - timeSort: 'desc', - num: pageNum, - size: pageSize, - }, - }); - let payload; - try { - payload = await request(config); - } catch (err) { - if (/token|unauthor|401/i.test(String(err?.message || ''))) { - const refreshed = await ensureCloudMailToken(latestState, { forceRefresh: true }); - payload = await request(refreshed.config); - } else { - throw err; - } - } - const messages = normalizeCloudMailMailApiMessages(payload).filter((message) => { - if (!address) return true; - return !message.address || normalizeCloudMailAddress(message.address) === address; - }); - return { config, messages }; -} - -async function pollCloudMailVerificationCode(step, state, pollPayload = {}) { - const latestState = state || await getState(); - const config = ensureCloudMailConfig(latestState, { requireCredentials: true }); - const targetEmail = resolveCloudMailPollTargetEmail(latestState, pollPayload, config); - const registrationEmail = normalizeCloudMailReceiveMailbox(latestState.email); - if (!targetEmail) { - throw new Error('Cloud Mail 轮询前缺少目标邮箱地址,请先填写注册邮箱或"邮件接收"邮箱。'); - } - if (registrationEmail && registrationEmail !== targetEmail) { - await addLog(`步骤 ${step}:正在轮询 Cloud Mail 收件邮箱(${targetEmail}),注册邮箱为 ${registrationEmail}...`, 'info'); - } else { - await addLog(`步骤 ${step}:正在轮询 Cloud Mail 邮件(${targetEmail})...`, 'info'); - } - const maxAttempts = Number(pollPayload.maxAttempts) || 5; - const intervalMs = Number(pollPayload.intervalMs) || 3000; - let lastError = null; - for (let attempt = 1; attempt <= maxAttempts; attempt++) { - throwIfStopped(); - try { - const { messages } = await listCloudMailMessages(latestState, { - address: targetEmail, - limit: pollPayload.limit || CLOUD_MAIL_DEFAULT_PAGE_SIZE, - page: pollPayload.page || 1, - }); - const matchResult = pickVerificationMessageWithTimeFallback(messages, { - afterTimestamp: pollPayload.filterAfterTimestamp || 0, - senderFilters: pollPayload.senderFilters || [], - subjectFilters: pollPayload.subjectFilters || [], - excludeCodes: pollPayload.excludeCodes || [], - }); - const match = matchResult.match; - if (match?.code) { - if (matchResult.usedRelaxedFilters) { - const fallbackLabel = matchResult.usedTimeFallback ? '宽松匹配 + 时间回退' : '宽松匹配'; - await addLog(`步骤 ${step}:严格规则未命中,已改用 ${fallbackLabel} 并命中 Cloud Mail 验证码。`, 'warn'); - } - return { - ok: true, - code: match.code, - emailTimestamp: match.receivedAt || Date.now(), - mailId: match.message?.id || '', - }; - } - lastError = new Error(`步骤 ${step}:暂未在 Cloud Mail 中找到匹配验证码(${attempt}/${maxAttempts})。`); - await addLog(lastError.message, attempt === maxAttempts ? 'warn' : 'info'); - const sample = summarizeCloudMailMessagesForLog(messages); - if (sample) { - await addLog(`步骤 ${step}:最近邮件样本:${sample}`, 'info'); - } - } catch (err) { - lastError = err; - await addLog(`步骤 ${step}:Cloud Mail 轮询失败:${err.message}`, 'warn'); - } - if (attempt < maxAttempts) { - await sleepWithStop(intervalMs); - } - } - throw lastError || new Error(`步骤 ${step}:未在 Cloud Mail 中找到新的匹配验证码。`); -} - async function getOpenIcloudHostPreference() { try { const tabs = await chrome.tabs.query({ @@ -7102,6 +6857,7 @@ function normalizeSub2ApiUrl(rawUrl) { return navigationUtils.normalizeSub2ApiUrl(rawUrl); } const input = (rawUrl || '').trim() || DEFAULT_SUB2API_URL; + if (!input) return ''; const withProtocol = /^https?:\/\//i.test(input) ? input : `https://${input}`; const parsed = new URL(withProtocol); if (!parsed.pathname || parsed.pathname === '/') { diff --git a/background/cloudmail-provider.js b/background/cloudmail-provider.js new file mode 100644 index 0000000..9c8f01d --- /dev/null +++ b/background/cloudmail-provider.js @@ -0,0 +1,324 @@ +(function cloudMailProviderModule(root, factory) { + root.MultiPageBackgroundCloudMailProvider = factory(); +})(typeof self !== 'undefined' ? self : globalThis, function createCloudMailProviderModule() { + function createCloudMailProvider(deps = {}) { + const { + addLog = async () => {}, + buildCloudMailHeaders, + CLOUD_MAIL_DEFAULT_PAGE_SIZE = 20, + CLOUD_MAIL_GENERATOR = 'cloudmail', + CLOUD_MAIL_PROVIDER = 'cloudmail', + fetchImpl = typeof fetch === 'function' ? fetch.bind(globalThis) : null, + getCloudMailTokenFromResponse, + getState = async () => ({}), + joinCloudMailUrl, + normalizeCloudMailAddress, + normalizeCloudMailBaseUrl, + normalizeCloudMailDomain, + normalizeCloudMailDomains, + normalizeCloudMailMailApiMessages, + pickVerificationMessageWithTimeFallback, + setEmailState = async () => {}, + setPersistentSettings = async () => {}, + sleepWithStop = async () => {}, + throwIfStopped = () => {}, + } = deps; + + function getCloudMailConfig(state = {}) { + return { + baseUrl: normalizeCloudMailBaseUrl(state.cloudMailBaseUrl), + adminEmail: String(state.cloudMailAdminEmail || '').trim(), + adminPassword: String(state.cloudMailAdminPassword || ''), + token: String(state.cloudMailToken || '').trim(), + receiveMailbox: normalizeCloudMailReceiveMailbox(state.cloudMailReceiveMailbox), + domain: normalizeCloudMailDomain(state.cloudMailDomain), + domains: normalizeCloudMailDomains(state.cloudMailDomains), + }; + } + + function normalizeCloudMailReceiveMailbox(value = '') { + const normalized = normalizeCloudMailAddress(value); + if (!normalized) return ''; + return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(normalized) ? normalized : ''; + } + + function resolveCloudMailPollTargetEmail(state = {}, pollPayload = {}, config = getCloudMailConfig(state)) { + const configuredReceiveMailbox = normalizeCloudMailReceiveMailbox(config.receiveMailbox); + const mailProvider = String(state?.mailProvider || '').trim().toLowerCase(); + const emailGenerator = String(state?.emailGenerator || '').trim().toLowerCase(); + const shouldPreferConfiguredReceiveMailbox = mailProvider === CLOUD_MAIL_PROVIDER + && emailGenerator !== CLOUD_MAIL_GENERATOR; + if (shouldPreferConfiguredReceiveMailbox && configuredReceiveMailbox) { + return configuredReceiveMailbox; + } + + const requestedTarget = normalizeCloudMailReceiveMailbox(pollPayload.targetEmail); + if (requestedTarget) { + return requestedTarget; + } + + return normalizeCloudMailReceiveMailbox(state.email); + } + + function ensureCloudMailConfig(state, options = {}) { + const { requireToken = false, requireCredentials = false, requireDomain = false } = options; + const config = getCloudMailConfig(state); + if (!config.baseUrl) { + throw new Error('Cloud Mail 服务地址为空或格式无效。'); + } + if (requireCredentials && (!config.adminEmail || !config.adminPassword)) { + throw new Error('Cloud Mail 缺少管理员邮箱或密码。'); + } + if (requireToken && !config.token) { + throw new Error('Cloud Mail 尚未获取到身份令牌,请先生成 Token。'); + } + if (requireDomain && !config.domain) { + throw new Error('Cloud Mail 域名为空或格式无效。'); + } + return config; + } + + async function requestCloudMailJson(config, path, options = {}) { + if (!fetchImpl) { + throw new Error('Cloud Mail 当前运行环境不支持 fetch。'); + } + const { + method = 'POST', + payload, + timeoutMs = 20000, + requireToken = true, + } = options; + const url = joinCloudMailUrl(config.baseUrl, path); + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(new Error('timeout')), timeoutMs); + let response; + try { + response = await fetchImpl(url, { + method, + headers: buildCloudMailHeaders(config, { + json: payload !== undefined, + token: requireToken ? undefined : '', + }), + body: payload !== undefined ? JSON.stringify(payload) : undefined, + signal: controller.signal, + }); + } catch (err) { + const errorMessage = err?.name === 'AbortError' + ? `Cloud Mail 请求超时(>${Math.round(timeoutMs / 1000)} 秒)` + : `Cloud Mail 请求失败:${err.message}`; + throw new Error(errorMessage); + } finally { + clearTimeout(timeoutId); + } + const text = await response.text(); + let parsed; + try { + parsed = text ? JSON.parse(text) : {}; + } catch { + parsed = text; + } + if (!response.ok) { + const payloadError = typeof parsed === 'object' && parsed + ? (parsed.message || parsed.error || parsed.msg) + : ''; + throw new Error(`Cloud Mail 请求失败:${payloadError || text || `HTTP ${response.status}`}`); + } + if (parsed && typeof parsed === 'object' && 'code' in parsed && Number(parsed.code) !== 200) { + throw new Error(`Cloud Mail 业务错误:${parsed.message || parsed.msg || `code=${parsed.code}`}`); + } + return parsed; + } + + async function ensureCloudMailToken(state, options = {}) { + const { forceRefresh = false } = options; + const latestState = state || await getState(); + const config = ensureCloudMailConfig(latestState, { requireCredentials: true }); + if (!forceRefresh && config.token) { + return { config, token: config.token }; + } + const loginConfig = { ...config, token: '' }; + const result = await requestCloudMailJson(loginConfig, '/api/public/genToken', { + method: 'POST', + payload: { email: config.adminEmail, password: config.adminPassword }, + requireToken: false, + }); + const token = getCloudMailTokenFromResponse(result); + if (!token) { + throw new Error('Cloud Mail 未返回可用 Token。'); + } + await setPersistentSettings({ cloudMailToken: token }); + return { config: { ...config, token }, token }; + } + + function generateCloudMailAliasLocalPart() { + const letters = 'abcdefghijklmnopqrstuvwxyz'; + const digits = '0123456789'; + const chars = []; + for (let i = 0; i < 6; i++) chars.push(letters[Math.floor(Math.random() * letters.length)]); + for (let i = 0; i < 4; i++) chars.push(digits[Math.floor(Math.random() * digits.length)]); + for (let i = chars.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [chars[i], chars[j]] = [chars[j], chars[i]]; + } + return chars.join(''); + } + + async function fetchCloudMailAddress(state, options = {}) { + throwIfStopped(); + const latestState = state || await getState(); + const { config } = await ensureCloudMailToken(latestState); + const ensuredConfig = ensureCloudMailConfig({ ...latestState, cloudMailToken: config.token }, { + requireToken: true, + requireDomain: true, + }); + const requestedLocal = String(options.localPart || options.name || '').trim().toLowerCase() + || generateCloudMailAliasLocalPart(); + const address = `${requestedLocal}@${ensuredConfig.domain}`.toLowerCase(); + const payload = { list: [{ email: address }] }; + try { + await requestCloudMailJson(ensuredConfig, '/api/public/addUser', { method: 'POST', payload }); + } catch (err) { + if (/token|unauthor|401/i.test(String(err?.message || ''))) { + const refreshed = await ensureCloudMailToken(latestState, { forceRefresh: true }); + await requestCloudMailJson(refreshed.config, '/api/public/addUser', { method: 'POST', payload }); + } else { + throw err; + } + } + await setEmailState(address); + await addLog(`Cloud Mail:已生成 ${address}`, 'ok'); + return address; + } + + function summarizeCloudMailMessagesForLog(messages) { + return (messages || []) + .slice() + .sort((left, right) => { + const leftTime = Date.parse(left.receivedDateTime || '') || 0; + const rightTime = Date.parse(right.receivedDateTime || '') || 0; + return rightTime - leftTime; + }) + .slice(0, 3) + .map((message) => { + const receivedAt = message?.receivedDateTime || '未知时间'; + const sender = message?.from?.emailAddress?.address || '未知发件人'; + const subject = message?.subject || '(无主题)'; + const preview = String(message?.bodyPreview || '').replace(/\s+/g, ' ').trim().slice(0, 80); + const address = message?.address || '未知地址'; + return `[${address}] ${receivedAt} | ${sender} | ${subject} | ${preview}`; + }) + .join(' || '); + } + + async function listCloudMailMessages(state, options = {}) { + const latestState = state || await getState(); + const { config } = await ensureCloudMailToken(latestState); + const address = normalizeCloudMailAddress(options.address); + const pageSize = Number(options.limit) || CLOUD_MAIL_DEFAULT_PAGE_SIZE; + const pageNum = Number(options.page) || 1; + const request = async (currentConfig) => requestCloudMailJson(currentConfig, '/api/public/emailList', { + method: 'POST', + payload: { + toEmail: address || undefined, + type: 0, + isDel: 0, + timeSort: 'desc', + num: pageNum, + size: pageSize, + }, + }); + let payload; + try { + payload = await request(config); + } catch (err) { + if (/token|unauthor|401/i.test(String(err?.message || ''))) { + const refreshed = await ensureCloudMailToken(latestState, { forceRefresh: true }); + payload = await request(refreshed.config); + } else { + throw err; + } + } + const messages = normalizeCloudMailMailApiMessages(payload).filter((message) => { + if (!address) return true; + return !message.address || normalizeCloudMailAddress(message.address) === address; + }); + return { config, messages }; + } + + async function pollCloudMailVerificationCode(step, state, pollPayload = {}) { + const latestState = state || await getState(); + const config = ensureCloudMailConfig(latestState, { requireCredentials: true }); + const targetEmail = resolveCloudMailPollTargetEmail(latestState, pollPayload, config); + const registrationEmail = normalizeCloudMailReceiveMailbox(latestState.email); + if (!targetEmail) { + throw new Error('Cloud Mail 轮询前缺少目标邮箱地址,请先填写注册邮箱或"邮件接收"邮箱。'); + } + if (registrationEmail && registrationEmail !== targetEmail) { + await addLog(`步骤 ${step}:正在轮询 Cloud Mail 收件邮箱(${targetEmail}),注册邮箱为 ${registrationEmail}...`, 'info'); + } else { + await addLog(`步骤 ${step}:正在轮询 Cloud Mail 邮件(${targetEmail})...`, 'info'); + } + const maxAttempts = Number(pollPayload.maxAttempts) || 5; + const intervalMs = Number(pollPayload.intervalMs) || 3000; + let lastError = null; + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + throwIfStopped(); + try { + const { messages } = await listCloudMailMessages(latestState, { + address: targetEmail, + limit: pollPayload.limit || CLOUD_MAIL_DEFAULT_PAGE_SIZE, + page: pollPayload.page || 1, + }); + const matchResult = pickVerificationMessageWithTimeFallback(messages, { + afterTimestamp: pollPayload.filterAfterTimestamp || 0, + senderFilters: pollPayload.senderFilters || [], + subjectFilters: pollPayload.subjectFilters || [], + excludeCodes: pollPayload.excludeCodes || [], + }); + const match = matchResult.match; + if (match?.code) { + if (matchResult.usedRelaxedFilters) { + const fallbackLabel = matchResult.usedTimeFallback ? '宽松匹配 + 时间回退' : '宽松匹配'; + await addLog(`步骤 ${step}:严格规则未命中,已改用 ${fallbackLabel} 并命中 Cloud Mail 验证码。`, 'warn'); + } + return { + ok: true, + code: match.code, + emailTimestamp: match.receivedAt || Date.now(), + mailId: match.message?.id || '', + }; + } + lastError = new Error(`步骤 ${step}:暂未在 Cloud Mail 中找到匹配验证码(${attempt}/${maxAttempts})。`); + await addLog(lastError.message, attempt === maxAttempts ? 'warn' : 'info'); + const sample = summarizeCloudMailMessagesForLog(messages); + if (sample) { + await addLog(`步骤 ${step}:最近邮件样本:${sample}`, 'info'); + } + } catch (err) { + lastError = err; + await addLog(`步骤 ${step}:Cloud Mail 轮询失败:${err.message}`, 'warn'); + } + if (attempt < maxAttempts) { + await sleepWithStop(intervalMs); + } + } + throw lastError || new Error(`步骤 ${step}:未在 Cloud Mail 中找到新的匹配验证码。`); + } + + return { + ensureCloudMailConfig, + ensureCloudMailToken, + fetchCloudMailAddress, + getCloudMailConfig, + listCloudMailMessages, + normalizeCloudMailReceiveMailbox, + pollCloudMailVerificationCode, + requestCloudMailJson, + resolveCloudMailPollTargetEmail, + }; + } + + return { + createCloudMailProvider, + }; +}); diff --git a/background/mail-2925-session.js b/background/mail-2925-session.js index e80dc4c..e0e79cd 100644 --- a/background/mail-2925-session.js +++ b/background/mail-2925-session.js @@ -37,12 +37,10 @@ const MAIL2925_COOKIE_DOMAINS = [ '2925.com', 'www.2925.com', - 'mail2.xiyouji.com', ]; const MAIL2925_COOKIE_ORIGINS = [ 'https://2925.com', 'https://www.2925.com', - 'https://mail2.xiyouji.com', ]; const MAIL2925_LIMIT_ERROR_PREFIX = 'MAIL2925_LIMIT_REACHED::'; const MAIL2925_THREAD_TERMINATED_ERROR_PREFIX = 'MAIL2925_THREAD_TERMINATED::'; diff --git a/background/navigation-utils.js b/background/navigation-utils.js index f1ceef3..9077502 100644 --- a/background/navigation-utils.js +++ b/background/navigation-utils.js @@ -19,6 +19,7 @@ function normalizeSub2ApiUrl(rawUrl) { const input = (rawUrl || '').trim() || DEFAULT_SUB2API_URL; + if (!input) return ''; const withProtocol = /^https?:\/\//i.test(input) ? input : `https://${input}`; const parsed = new URL(withProtocol); if (!parsed.pathname || parsed.pathname === '/') { diff --git a/background/panel-bridge.js b/background/panel-bridge.js index a4dff4c..8e204b5 100644 --- a/background/panel-bridge.js +++ b/background/panel-bridge.js @@ -251,6 +251,9 @@ const sub2apiUrl = normalizeSub2ApiUrl(state.sub2apiUrl); const groupName = (state.sub2apiGroupName || DEFAULT_SUB2API_GROUP_NAME).trim() || DEFAULT_SUB2API_GROUP_NAME; + if (!sub2apiUrl) { + throw new Error('SUB2API URL is not configured. Please fill it in the side panel first.'); + } if (!state.sub2apiEmail) { throw new Error('尚未配置 SUB2API 登录邮箱,请先在侧边栏填写。'); } diff --git a/background/steps/oauth-login.js b/background/steps/oauth-login.js index 07eb041..7c8e0bd 100644 --- a/background/steps/oauth-login.js +++ b/background/steps/oauth-login.js @@ -40,29 +40,64 @@ return /缺少|未配置|请输入|无效|错误|失败|401|认证失败|未授权|unauthorized|invalid/i.test(message); } + function normalizeStep7IdentifierType(value = '') { + const normalized = String(value || '').trim().toLowerCase(); + return normalized === 'phone' || normalized === 'email' ? normalized : ''; + } + + function normalizeStep7SignupMethod(value = '') { + return String(value || '').trim().toLowerCase() === 'phone' ? 'phone' : 'email'; + } + + function canUseConfiguredPhoneSignup(state = {}) { + return normalizeStep7SignupMethod(state?.signupMethod) === 'phone' + && Boolean(state?.phoneVerificationEnabled) + && !Boolean(state?.plusModeEnabled) + && !Boolean(state?.contributionMode); + } + + function resolveStep7LoginIdentifierType(state = {}, fallbackType = '') { + const explicitIdentifierType = normalizeStep7IdentifierType(state?.accountIdentifierType); + if (explicitIdentifierType) { + return explicitIdentifierType; + } + + const frozenSignupMethod = normalizeStep7IdentifierType(state?.resolvedSignupMethod); + if (frozenSignupMethod) { + return frozenSignupMethod; + } + + if (canUseConfiguredPhoneSignup(state)) { + return 'phone'; + } + + return normalizeStep7IdentifierType(fallbackType) || 'email'; + } + async function executeStep7(state) { const visibleStep = Math.floor(Number(state?.visibleStep) || 0); const completionStep = visibleStep > 0 ? visibleStep : 7; - const resolvedIdentifierType = String( - state?.accountIdentifierType - || (state?.signupPhoneNumber ? 'phone' : '') - || '' - ).trim().toLowerCase() === 'phone' - ? 'phone' - : 'email'; - const phoneNumber = String( - state?.signupPhoneNumber - || (resolvedIdentifierType === 'phone' ? state?.accountIdentifier : '') - || state?.signupPhoneCompletedActivation?.phoneNumber - || state?.signupPhoneActivation?.phoneNumber - || '' - ).trim(); - const email = String( - state?.email - || (resolvedIdentifierType === 'email' ? state?.accountIdentifier : '') - || '' - ).trim(); - if (!email && !phoneNumber) { + const resolvedIdentifierType = resolveStep7LoginIdentifierType(state); + const phoneNumber = resolvedIdentifierType === 'phone' + ? String( + state?.signupPhoneNumber + || (normalizeStep7IdentifierType(state?.accountIdentifierType) === 'phone' ? state?.accountIdentifier : '') + || state?.signupPhoneCompletedActivation?.phoneNumber + || state?.signupPhoneActivation?.phoneNumber + || '' + ).trim() + : ''; + const email = resolvedIdentifierType === 'email' + ? String( + state?.email + || (normalizeStep7IdentifierType(state?.accountIdentifierType) === 'email' ? state?.accountIdentifier : '') + || '' + ).trim() + : ''; + if ( + (resolvedIdentifierType === 'phone' && !phoneNumber) + || (resolvedIdentifierType !== 'phone' && !email) + ) { throw new Error('缺少登录账号:请先完成步骤 2,或在侧栏“注册邮箱/注册手机号”中手动填写账号后再执行当前步骤。'); } @@ -75,25 +110,23 @@ try { const currentState = attempt === 1 ? state : await getState(); const password = currentState.password || currentState.customPassword || ''; - const currentIdentifierType = String( - currentState?.accountIdentifierType - || (currentState?.signupPhoneNumber ? 'phone' : '') - || resolvedIdentifierType - ).trim().toLowerCase() === 'phone' - ? 'phone' - : 'email'; - const currentPhoneNumber = String( - currentState?.signupPhoneNumber - || (currentIdentifierType === 'phone' ? currentState?.accountIdentifier : '') - || currentState?.signupPhoneCompletedActivation?.phoneNumber - || currentState?.signupPhoneActivation?.phoneNumber - || phoneNumber - ).trim(); - const currentEmail = String( - currentState?.email - || (currentIdentifierType === 'email' ? currentState?.accountIdentifier : '') - || email - ).trim(); + const currentIdentifierType = resolveStep7LoginIdentifierType(currentState, resolvedIdentifierType); + const currentPhoneNumber = currentIdentifierType === 'phone' + ? String( + currentState?.signupPhoneNumber + || (normalizeStep7IdentifierType(currentState?.accountIdentifierType) === 'phone' ? currentState?.accountIdentifier : '') + || currentState?.signupPhoneCompletedActivation?.phoneNumber + || currentState?.signupPhoneActivation?.phoneNumber + || phoneNumber + ).trim() + : ''; + const currentEmail = currentIdentifierType === 'email' + ? String( + currentState?.email + || (normalizeStep7IdentifierType(currentState?.accountIdentifierType) === 'email' ? currentState?.accountIdentifier : '') + || email + ).trim() + : ''; const accountIdentifier = currentIdentifierType === 'phone' ? currentPhoneNumber : currentEmail; diff --git a/background/steps/platform-verify.js b/background/steps/platform-verify.js index e9fb06c..2d39999 100644 --- a/background/steps/platform-verify.js +++ b/background/steps/platform-verify.js @@ -343,6 +343,9 @@ } const sub2apiUrl = normalizeSub2ApiUrl(state.sub2apiUrl); + if (!sub2apiUrl) { + throw new Error('SUB2API URL is not configured. Please fill it in the side panel first.'); + } const injectFiles = ['content/utils.js', 'content/sub2api-panel.js']; await addStepLog(visibleStep, '正在打开 SUB2API 后台...'); diff --git a/cloudmail-utils.js b/cloudmail-utils.js index 879d224..f43d0f9 100644 --- a/cloudmail-utils.js +++ b/cloudmail-utils.js @@ -132,7 +132,7 @@ const source = String(value).trim(); if (!source) return ''; - // Cloud Mail returns UTC time like "2099-12-30 23:59:59"; treat as UTC + // Cloud Mail returns UTC time like "2099-12-30 23:59:59"; treat as UTC. const match = source.match(/^(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2}):(\d{2})(?:\.(\d+))?(?:Z|([+-]\d{2}:?\d{2}))?$/); if (match) { const [, year, month, day, hour, minute, second, ms, offset] = match; @@ -225,231 +225,4 @@ normalizeCloudMailMailApiMessages, normalizeCloudMailMessage, }; -});(function cloudMailUtilsModule(root, factory) { - if (typeof module !== 'undefined' && module.exports) { - module.exports = factory(); - return; - } - - root.CloudMailUtils = factory(); -})(typeof self !== 'undefined' ? self : globalThis, function createCloudMailUtils() { - const DEFAULT_MAIL_PAGE_SIZE = 20; - - function firstNonEmptyString(values) { - for (const value of values) { - if (value === undefined || value === null) continue; - const normalized = String(value).trim(); - if (normalized) return normalized; - } - return ''; - } - - function normalizeCloudMailBaseUrl(rawValue = '') { - const value = String(rawValue || '').trim(); - if (!value) return ''; - - const candidate = /^[a-zA-Z][a-zA-Z\d+\-.]*:\/\//.test(value) ? value : `https://${value}`; - try { - const parsed = new URL(candidate); - parsed.hash = ''; - parsed.search = ''; - const pathname = parsed.pathname === '/' ? '' : parsed.pathname.replace(/\/+$/, ''); - return `${parsed.origin}${pathname}`; - } catch { - return ''; - } - } - - function normalizeCloudMailDomain(rawValue = '') { - let value = String(rawValue || '').trim().toLowerCase(); - if (!value) return ''; - value = value.replace(/^@+/, ''); - value = value.replace(/^https?:\/\//, ''); - value = value.replace(/\/.*$/, ''); - if (!/^[a-z0-9.-]+\.[a-z]{2,}$/i.test(value)) { - return ''; - } - return value; - } - - function normalizeCloudMailDomains(values) { - const domains = []; - const seen = new Set(); - for (const value of Array.isArray(values) ? values : []) { - const normalized = normalizeCloudMailDomain(value); - if (!normalized || seen.has(normalized)) continue; - seen.add(normalized); - domains.push(normalized); - } - return domains; - } - - function buildCloudMailHeaders(config = {}, options = {}) { - const headers = {}; - const token = firstNonEmptyString([ - config.token, - config.cloudMailToken, - options.token, - ]); - if (token) { - headers.Authorization = token; - } - if (options.json) { - headers['Content-Type'] = 'application/json'; - } - if (options.acceptJson !== false) { - headers.Accept = 'application/json'; - } - return headers; - } - - function joinCloudMailUrl(baseUrl, path) { - const normalizedBase = normalizeCloudMailBaseUrl(baseUrl); - const normalizedPath = String(path || '').trim(); - if (!normalizedBase || !normalizedPath) return normalizedBase || ''; - return `${normalizedBase}${normalizedPath.startsWith('/') ? '' : '/'}${normalizedPath}`; - } - - function getCloudMailMailRows(payload) { - if (Array.isArray(payload)) return payload; - if (!payload || typeof payload !== 'object') return []; - - const candidates = [ - payload.data, - payload.list, - payload.items, - payload.rows, - payload.records, - payload?.data?.list, - payload?.data?.records, - payload?.data?.rows, - ]; - - for (const candidate of candidates) { - if (Array.isArray(candidate)) { - return candidate; - } - } - - return []; - } - - function normalizeCloudMailAddress(value) { - return String(value || '').trim().toLowerCase(); - } - - function stripHtmlTags(value = '') { - return String(value || '') - .replace(//gi, ' ') - .replace(//gi, ' ') - .replace(/<[^>]+>/g, ' ') - .replace(/ /gi, ' ') - .replace(/&/gi, '&') - .replace(//gi, '>') - .replace(/\s+/g, ' ') - .trim(); - } - - function parseCloudMailCreateTime(value) { - if (value === undefined || value === null || value === '') return ''; - if (typeof value === 'number' && Number.isFinite(value)) { - return new Date(value).toISOString(); - } - const source = String(value).trim(); - if (!source) return ''; - - // Cloud Mail returns UTC time like "2099-12-30 23:59:59"; treat as UTC - const match = source.match(/^(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2}):(\d{2})(?:\.(\d+))?(?:Z|([+-]\d{2}:?\d{2}))?$/); - if (match) { - const [, year, month, day, hour, minute, second, ms, offset] = match; - let iso = `${year}-${month}-${day}T${hour}:${minute}:${second}`; - if (ms) iso += `.${ms}`; - if (offset) { - iso += offset.includes(':') ? offset : `${offset.slice(0, 3)}:${offset.slice(3)}`; - } else { - iso += 'Z'; - } - const parsed = Date.parse(iso); - return Number.isFinite(parsed) ? new Date(parsed).toISOString() : iso; - } - - const parsed = Date.parse(source); - return Number.isFinite(parsed) ? new Date(parsed).toISOString() : source; - } - - function normalizeCloudMailMessage(row = {}) { - if (!row || typeof row !== 'object') return null; - - const address = normalizeCloudMailAddress(firstNonEmptyString([ - row.toEmail, - row.to_email, - row.recipient, - row.address, - row.email, - ])); - const subject = firstNonEmptyString([row.subject, row.title]); - const fromAddress = firstNonEmptyString([ - row.sendEmail, - row.send_email, - row.from, - row.sender, - row.mailFrom, - ]); - const htmlContent = firstNonEmptyString([row.content, row.html]); - const textContent = firstNonEmptyString([row.text, row.plainText, row.content_text]); - const bodyPreview = (textContent - || stripHtmlTags(htmlContent) - || '').replace(/\s+/g, ' ').trim(); - - return { - id: firstNonEmptyString([row.emailId, row.id, row.mailId, row.mail_id]), - address, - addressId: '', - subject, - from: { - emailAddress: { - address: fromAddress, - }, - }, - bodyPreview, - raw: htmlContent || textContent || '', - receivedDateTime: parseCloudMailCreateTime(firstNonEmptyString([ - row.createTime, - row.create_time, - row.createdAt, - row.created_at, - row.receivedDateTime, - row.date, - ])), - }; - } - - function normalizeCloudMailMailApiMessages(payload) { - return getCloudMailMailRows(payload) - .map((row) => normalizeCloudMailMessage(row)) - .filter(Boolean); - } - - function getCloudMailTokenFromResponse(payload = {}) { - return firstNonEmptyString([ - payload?.data?.token, - payload?.token, - payload?.data?.accessToken, - payload?.accessToken, - ]); - } - - return { - DEFAULT_MAIL_PAGE_SIZE, - buildCloudMailHeaders, - getCloudMailTokenFromResponse, - joinCloudMailUrl, - normalizeCloudMailAddress, - normalizeCloudMailBaseUrl, - normalizeCloudMailDomain, - normalizeCloudMailDomains, - normalizeCloudMailMailApiMessages, - normalizeCloudMailMessage, - }; -}); \ No newline at end of file +}); diff --git a/content/signup-page.js b/content/signup-page.js index 797236a..d78aea6 100644 --- a/content/signup-page.js +++ b/content/signup-page.js @@ -3484,6 +3484,10 @@ function summarizePhoneInputCandidate(element, options = {}) { const normalizedName = summary.name.toLowerCase(); const normalizedId = summary.id.toLowerCase(); const combinedText = `${normalizedName} ${normalizedId} ${summary.placeholder} ${summary.ariaLabel}`; + if (isLoginEmailLikeInput(element)) { + summary.skipReason = 'email_like'; + return summary; + } if ( summary.type === 'tel' || summary.autocomplete === 'tel' @@ -3517,14 +3521,50 @@ function findUsablePhoneInput(selector, options = {}) { .find((element) => isUsablePhoneInputElement(element, options)) || null; } +function getLoginInputAttributeText(input) { + return { + type: String(input?.getAttribute?.('type') || input?.type || '').trim().toLowerCase(), + autocomplete: String(input?.getAttribute?.('autocomplete') || '').trim().toLowerCase(), + name: String(input?.getAttribute?.('name') || input?.name || '').trim(), + id: String(input?.getAttribute?.('id') || input?.id || '').trim(), + placeholder: String(input?.getAttribute?.('placeholder') || '').trim(), + ariaLabel: String(input?.getAttribute?.('aria-label') || '').trim(), + }; +} + +function isLoginEmailLikeInput(input) { + const summary = getLoginInputAttributeText(input); + const nameId = `${summary.name} ${summary.id}`; + const labelText = `${summary.placeholder} ${summary.ariaLabel}`; + return summary.type === 'email' + || summary.autocomplete === 'email' + || /email/i.test(nameId) + || /email|电子邮件|邮箱/i.test(labelText); +} + function getLoginEmailInput() { - const input = document.querySelector( - 'input[type="email"], input[name="email"], input[name="username"], input[id*="email"], input[placeholder*="email" i], input[placeholder*="Email"]' - ); - if (isLoginPhoneUsernameKind() || isLoginPhoneEntryPageText()) { + const input = Array.from(document.querySelectorAll([ + 'input[type="email"]', + 'input[autocomplete="email"]', + 'input[name="email"]', + 'input[name="username"]', + 'input[autocomplete="username"]', + 'input[id*="email" i]', + 'input[placeholder*="email" i]', + 'input[placeholder*="Email"]', + 'input[placeholder*="电子邮件"]', + 'input[placeholder*="邮箱"]', + 'input[aria-label*="email" i]', + 'input[aria-label*="电子邮件"]', + 'input[aria-label*="邮箱"]', + ].join(', '))).find((candidate) => isVisibleElement(candidate)) || null; + if (!input) { return null; } - return input && isVisibleElement(input) ? input : null; + if ((isLoginPhoneUsernameKind() || isLoginPhoneEntryPageText()) && !isLoginEmailLikeInput(input)) { + return null; + } + return input; } function getLoginPhoneInput() { @@ -5162,9 +5202,9 @@ async function step6OpenLoginEntry(payload, snapshot) { const visibleStep = Math.floor(Number(payload?.visibleStep) || 0) || 7; const currentSnapshot = normalizeStep6Snapshot(snapshot || inspectLoginAuthState()); const preferPhoneLogin = String(payload?.loginIdentifierType || '').trim() === 'phone' || (!payload?.email && payload?.phoneNumber); - const trigger = preferPhoneLogin - ? (currentSnapshot.phoneEntryTrigger || findLoginPhoneEntryTrigger()) - : (currentSnapshot.loginEntryTrigger || findLoginEntryTrigger()); + const genericEntryTrigger = currentSnapshot.loginEntryTrigger || findLoginEntryTrigger(); + const phoneEntryTrigger = currentSnapshot.phoneEntryTrigger || findLoginPhoneEntryTrigger(); + const trigger = genericEntryTrigger || (preferPhoneLogin ? phoneEntryTrigger : null); if (!trigger || !isActionEnabled(trigger)) { return createStep6RecoverableResult('missing_login_entry_trigger', currentSnapshot, { message: preferPhoneLogin diff --git a/gopay-utils.js b/gopay-utils.js index 0fe3e9b..b2c2eee 100644 --- a/gopay-utils.js +++ b/gopay-utils.js @@ -5,7 +5,7 @@ const PLUS_PAYMENT_METHOD_GOPAY = 'gopay'; const PLUS_PAYMENT_METHOD_GPC_HELPER = 'gpc-helper'; const DEFAULT_GPC_HELPER_API_URL = 'https://gpc.qlhazycoder.top'; - const LEGACY_GPC_HELPER_API_URL = 'https://gpc.leftcode.xyz'; + const ALLOWED_GPC_HELPER_REMOTE_HOST = 'gpc.qlhazycoder.top'; function normalizePlusPaymentMethod(value = '') { const normalized = String(value || '').trim().toLowerCase(); @@ -67,10 +67,17 @@ normalized = normalized.replace(/\/api\/gp\/balance(?:\?.*)?$/i, ''); normalized = normalized.replace(/\/api\/card\/balance(?:\?.*)?$/i, ''); normalized = normalized.replace(/\/api\/card\/redeem-api-key(?:\?.*)?$/i, ''); - if (normalized === LEGACY_GPC_HELPER_API_URL) { + + try { + const parsed = new URL(normalized); + const hostname = parsed.hostname.toLowerCase(); + if (hostname === ALLOWED_GPC_HELPER_REMOTE_HOST || hostname === 'localhost' || hostname === '127.0.0.1') { + return normalized || DEFAULT_GPC_HELPER_API_URL; + } + return DEFAULT_GPC_HELPER_API_URL; + } catch { return DEFAULT_GPC_HELPER_API_URL; } - return normalized || DEFAULT_GPC_HELPER_API_URL; } function buildGpcHelperApiUrl(apiUrl = '', path = '') { diff --git a/hotmail-utils.js b/hotmail-utils.js index 14103c6..3ff4778 100644 --- a/hotmail-utils.js +++ b/hotmail-utils.js @@ -6,7 +6,6 @@ root.HotmailUtils = factory(); })(typeof self !== 'undefined' ? self : globalThis, function createHotmailUtils() { - const HOTMAIL_MAIL_API_URL = 'https://apple.882263.xyz/api/mail-new'; const HOTMAIL_SERVICE_MODE_REMOTE = 'remote'; const HOTMAIL_SERVICE_MODE_LOCAL = 'local'; @@ -287,8 +286,11 @@ return list.map((message) => normalizeHotmailMailApiMessage(message)); } - function buildHotmailMailApiLatestUrl(options) { - const apiUrl = String(options?.apiUrl || '').trim() || HOTMAIL_MAIL_API_URL; + function buildHotmailMailApiLatestUrl(options = {}) { + const apiUrl = String(options?.apiUrl || '').trim(); + if (!apiUrl) { + throw new Error('Hotmail mail API URL is required.'); + } const url = new URL(apiUrl); url.searchParams.set('refresh_token', String(options?.refreshToken || '')); url.searchParams.set('client_id', String(options?.clientId || '')); diff --git a/manifest.json b/manifest.json index 0532b5f..33d7996 100644 --- a/manifest.json +++ b/manifest.json @@ -1,8 +1,8 @@ { "manifest_version": 3, "name": "codex-oauth-automation-extension", - "version": "7.0", - "version_name": "Ultra7.0", + "version": "7.3", + "version_name": "Ultra7.3", "description": "用于自动执行多步骤 OAuth 注册流程", "permissions": [ "sidePanel", diff --git a/sidepanel/sidepanel.html b/sidepanel/sidepanel.html index ac485c2..4a4e5bc 100644 --- a/sidepanel/sidepanel.html +++ b/sidepanel/sidepanel.html @@ -179,7 +179,7 @@