feat: support Cloud Mail email provider
- Merge the latest dev branch into PR #212 for a clean dev-targeted review base. - Add Cloud Mail/SkyMail provider support for email generation and verification-code polling. - Move Cloud Mail runtime logic into a dedicated background module and add docs plus regression tests.
This commit is contained in:
@@ -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://<your-inbucket-host>/m/<mailbox>/`
|
||||
|
||||
@@ -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
|
||||
|
||||
+42
-286
@@ -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 === '/') {
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
});
|
||||
@@ -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::';
|
||||
|
||||
@@ -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 === '/') {
|
||||
|
||||
@@ -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 登录邮箱,请先在侧边栏填写。');
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 后台...');
|
||||
|
||||
+2
-229
@@ -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(/<style[\s\S]*?<\/style>/gi, ' ')
|
||||
.replace(/<script[\s\S]*?<\/script>/gi, ' ')
|
||||
.replace(/<[^>]+>/g, ' ')
|
||||
.replace(/ /gi, ' ')
|
||||
.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,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
+48
-8
@@ -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
|
||||
|
||||
+10
-3
@@ -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 = '') {
|
||||
|
||||
+5
-3
@@ -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 || ''));
|
||||
|
||||
+2
-2
@@ -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",
|
||||
|
||||
@@ -179,7 +179,7 @@
|
||||
<div class="data-row" id="row-sub2api-url" style="display:none;">
|
||||
<span class="data-label">SUB2API</span>
|
||||
<input type="text" id="input-sub2api-url" class="data-input"
|
||||
placeholder="https://sub2api.hisence.fun/admin/accounts" />
|
||||
placeholder="https://your-sub2api-domain/admin/accounts" />
|
||||
</div>
|
||||
<div class="data-row" id="row-sub2api-email" style="display:none;">
|
||||
<span class="data-label">账号</span>
|
||||
@@ -406,10 +406,10 @@
|
||||
<option value="hotmail-api">Hotmail(账号池)</option>
|
||||
<option value="luckmail-api">LuckMail(API 购邮)</option>
|
||||
<option value="icloud">iCloud 邮箱</option>
|
||||
<option value="163">163 邮箱 (mail.163.com)</option>
|
||||
<option value="163-vip">163 VIP 邮箱 (vip.163.com)</option>
|
||||
<option value="126">126 邮箱 (mail.126.com)</option>
|
||||
<option value="qq">QQ 邮箱 (wx.mail.qq.com)</option>
|
||||
<option value="163">163 邮箱 (mail.163.com) 下个版本删除</option>
|
||||
<option value="163-vip">163 VIP 邮箱 (vip.163.com) 下个版本删除</option>
|
||||
<option value="126">126 邮箱 (mail.126.com) 下个版本删除</option>
|
||||
<option value="qq">QQ 邮箱 (wx.mail.qq.com) 下个版本删除</option>
|
||||
<option value="inbucket">Inbucket(自定义主机)</option>
|
||||
<option value="2925">2925 邮箱 (2925.com)</option>
|
||||
<option value="gmail">Gmail 邮箱 (mail.google.com)</option>
|
||||
|
||||
+12
-3
@@ -2748,6 +2748,15 @@ function collectSettingsPayload() {
|
||||
const selectedCloudflareTempEmailDomain = normalizeCloudflareTempEmailDomainValue(
|
||||
!cloudflareTempEmailDomainEditMode ? selectTempEmailDomain.value : tempEmailActiveDomain
|
||||
) || tempEmailActiveDomain;
|
||||
const normalizeCloudMailBaseUrlInput = typeof normalizeCloudMailBaseUrlValue === 'function'
|
||||
? normalizeCloudMailBaseUrlValue
|
||||
: normalizeCloudflareTempEmailBaseUrlValue;
|
||||
const normalizeCloudMailReceiveMailboxInput = typeof normalizeCloudMailReceiveMailboxValue === 'function'
|
||||
? normalizeCloudMailReceiveMailboxValue
|
||||
: normalizeCloudflareTempEmailReceiveMailboxValue;
|
||||
const normalizeCloudMailDomainInput = typeof normalizeCloudMailDomainValue === 'function'
|
||||
? normalizeCloudMailDomainValue
|
||||
: normalizeCloudflareTempEmailDomainValue;
|
||||
const contributionModeEnabled = Boolean(latestState?.contributionMode);
|
||||
const icloudFetchModeRawValue = typeof selectIcloudFetchMode !== 'undefined'
|
||||
? String(selectIcloudFetchMode?.value || '')
|
||||
@@ -3398,11 +3407,11 @@ function collectSettingsPayload() {
|
||||
cloudflareTempEmailUseRandomSubdomain: Boolean(inputTempEmailUseRandomSubdomain?.checked),
|
||||
cloudflareTempEmailDomain: selectedCloudflareTempEmailDomain,
|
||||
cloudflareTempEmailDomains: tempEmailDomains,
|
||||
cloudMailBaseUrl: normalizeCloudflareTempEmailBaseUrlValue((typeof inputCloudMailBaseUrl !== 'undefined' && inputCloudMailBaseUrl) ? inputCloudMailBaseUrl.value : ''),
|
||||
cloudMailBaseUrl: normalizeCloudMailBaseUrlInput((typeof inputCloudMailBaseUrl !== 'undefined' && inputCloudMailBaseUrl) ? inputCloudMailBaseUrl.value : ''),
|
||||
cloudMailAdminEmail: ((typeof inputCloudMailAdminEmail !== 'undefined' && inputCloudMailAdminEmail) ? inputCloudMailAdminEmail.value : '').trim(),
|
||||
cloudMailAdminPassword: (typeof inputCloudMailAdminPassword !== 'undefined' && inputCloudMailAdminPassword) ? inputCloudMailAdminPassword.value : '',
|
||||
cloudMailReceiveMailbox: normalizeCloudflareTempEmailReceiveMailboxValue((typeof inputCloudMailReceiveMailbox !== 'undefined' && inputCloudMailReceiveMailbox) ? inputCloudMailReceiveMailbox.value : ''),
|
||||
cloudMailDomain: normalizeCloudflareTempEmailDomainValue((typeof inputCloudMailDomain !== 'undefined' && inputCloudMailDomain) ? inputCloudMailDomain.value : ''),
|
||||
cloudMailReceiveMailbox: normalizeCloudMailReceiveMailboxInput((typeof inputCloudMailReceiveMailbox !== 'undefined' && inputCloudMailReceiveMailbox) ? inputCloudMailReceiveMailbox.value : ''),
|
||||
cloudMailDomain: normalizeCloudMailDomainInput((typeof inputCloudMailDomain !== 'undefined' && inputCloudMailDomain) ? inputCloudMailDomain.value : ''),
|
||||
autoRunSkipFailures: inputAutoSkipFailures.checked,
|
||||
autoRunFallbackThreadIntervalMinutes: normalizeAutoRunThreadIntervalMinutes(inputAutoSkipFailuresThreadIntervalMinutes.value),
|
||||
step6CookieCleanupEnabled: typeof inputStep6CookieCleanupEnabled !== 'undefined' && inputStep6CookieCleanupEnabled
|
||||
|
||||
@@ -71,6 +71,20 @@ test('navigation utils support codex2api mode and url normalization', () => {
|
||||
assert.equal(utils.getPanelModeLabel('codex2api'), 'Codex2API');
|
||||
});
|
||||
|
||||
test('navigation utils leaves SUB2API url empty when no default is configured', () => {
|
||||
const source = fs.readFileSync('background/navigation-utils.js', 'utf8');
|
||||
const globalScope = {};
|
||||
|
||||
const api = new Function('self', `${source}; return self.MultiPageBackgroundNavigationUtils;`)(globalScope);
|
||||
const utils = api.createNavigationUtils({
|
||||
DEFAULT_CODEX2API_URL: 'http://localhost:8080/admin/accounts',
|
||||
DEFAULT_SUB2API_URL: '',
|
||||
normalizeLocalCpaStep9Mode: (value) => value,
|
||||
});
|
||||
|
||||
assert.equal(utils.normalizeSub2ApiUrl(''), '');
|
||||
});
|
||||
|
||||
test('navigation utils recognize the iCloud mail tab family on both supported hosts', () => {
|
||||
const source = fs.readFileSync('background/navigation-utils.js', 'utf8');
|
||||
const globalScope = {};
|
||||
|
||||
@@ -358,6 +358,106 @@ test('step 7 forwards phone login identity payload when account identifier is ph
|
||||
]);
|
||||
});
|
||||
|
||||
test('step 7 keeps Plus email login even when phone sms runtime exists', async () => {
|
||||
const source = fs.readFileSync('background/steps/oauth-login.js', 'utf8');
|
||||
const globalScope = {};
|
||||
const api = new Function('self', `${source}; return self.MultiPageBackgroundStep7;`)(globalScope);
|
||||
|
||||
const events = {
|
||||
payloads: [],
|
||||
};
|
||||
|
||||
const executor = api.createStep7Executor({
|
||||
addLog: async () => {},
|
||||
completeStepFromBackground: async () => {},
|
||||
getErrorMessage: (error) => error?.message || String(error || ''),
|
||||
getLoginAuthStateLabel: (state) => state || 'unknown',
|
||||
getState: async () => ({
|
||||
plusModeEnabled: true,
|
||||
phoneVerificationEnabled: true,
|
||||
signupMethod: 'phone',
|
||||
email: 'plus.user@example.com',
|
||||
password: 'secret',
|
||||
signupPhoneNumber: '+441111111111',
|
||||
}),
|
||||
isStep6RecoverableResult: (result) => result?.step6Outcome === 'recoverable',
|
||||
isStep6SuccessResult: (result) => result?.step6Outcome === 'success',
|
||||
refreshOAuthUrlBeforeStep6: async () => 'https://oauth.example/latest',
|
||||
reuseOrCreateTab: async () => {},
|
||||
sendToContentScriptResilient: async (_sourceName, message) => {
|
||||
events.payloads.push(message.payload);
|
||||
return {
|
||||
step6Outcome: 'success',
|
||||
state: 'verification_page',
|
||||
loginVerificationRequestedAt: 123456,
|
||||
};
|
||||
},
|
||||
STEP6_MAX_ATTEMPTS: 3,
|
||||
throwIfStopped: () => {},
|
||||
});
|
||||
|
||||
await executor.executeStep7({
|
||||
plusModeEnabled: true,
|
||||
phoneVerificationEnabled: true,
|
||||
signupMethod: 'phone',
|
||||
email: 'plus.user@example.com',
|
||||
password: 'secret',
|
||||
signupPhoneNumber: '+441111111111',
|
||||
visibleStep: 10,
|
||||
});
|
||||
|
||||
assert.equal(events.payloads[0].loginIdentifierType, 'email');
|
||||
assert.equal(events.payloads[0].email, 'plus.user@example.com');
|
||||
assert.equal(events.payloads[0].phoneNumber, '');
|
||||
assert.equal(events.payloads[0].accountIdentifier, 'plus.user@example.com');
|
||||
});
|
||||
|
||||
test('step 7 can infer phone login from an available phone signup configuration before step 2 finishes', async () => {
|
||||
const source = fs.readFileSync('background/steps/oauth-login.js', 'utf8');
|
||||
const globalScope = {};
|
||||
const api = new Function('self', `${source}; return self.MultiPageBackgroundStep7;`)(globalScope);
|
||||
|
||||
const events = {
|
||||
payloads: [],
|
||||
};
|
||||
|
||||
const executor = api.createStep7Executor({
|
||||
addLog: async () => {},
|
||||
completeStepFromBackground: async () => {},
|
||||
getErrorMessage: (error) => error?.message || String(error || ''),
|
||||
getLoginAuthStateLabel: (state) => state || 'unknown',
|
||||
getState: async () => ({
|
||||
phoneVerificationEnabled: true,
|
||||
signupMethod: 'phone',
|
||||
signupPhoneNumber: '+447780579093',
|
||||
}),
|
||||
isStep6RecoverableResult: (result) => result?.step6Outcome === 'recoverable',
|
||||
isStep6SuccessResult: (result) => result?.step6Outcome === 'success',
|
||||
refreshOAuthUrlBeforeStep6: async () => 'https://oauth.example/latest',
|
||||
reuseOrCreateTab: async () => {},
|
||||
sendToContentScriptResilient: async (_sourceName, message) => {
|
||||
events.payloads.push(message.payload);
|
||||
return {
|
||||
step6Outcome: 'success',
|
||||
state: 'phone_verification_page',
|
||||
loginVerificationRequestedAt: 987654,
|
||||
};
|
||||
},
|
||||
STEP6_MAX_ATTEMPTS: 3,
|
||||
throwIfStopped: () => {},
|
||||
});
|
||||
|
||||
await executor.executeStep7({
|
||||
phoneVerificationEnabled: true,
|
||||
signupMethod: 'phone',
|
||||
signupPhoneNumber: '+447780579093',
|
||||
});
|
||||
|
||||
assert.equal(events.payloads[0].loginIdentifierType, 'phone');
|
||||
assert.equal(events.payloads[0].phoneNumber, '+447780579093');
|
||||
assert.equal(events.payloads[0].email, '');
|
||||
});
|
||||
|
||||
test('step 7 can start from a manually filled signup phone without completed step 2 or step 3 state', async () => {
|
||||
const source = fs.readFileSync('background/steps/oauth-login.js', 'utf8');
|
||||
const globalScope = {};
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
|
||||
require('../background/cloudmail-provider.js');
|
||||
|
||||
function createProviderApi(options = {}) {
|
||||
const {
|
||||
receiveMailbox = '',
|
||||
messages = [{
|
||||
id: 'mail-1',
|
||||
address: 'user@example.com',
|
||||
receivedDateTime: '2026-05-07T09:20:00.000Z',
|
||||
subject: 'OpenAI verification code',
|
||||
from: { emailAddress: { address: 'noreply@tm.openai.com' } },
|
||||
bodyPreview: 'Your verification code is 123456.',
|
||||
}],
|
||||
} = options;
|
||||
const logs = [];
|
||||
const listCalls = [];
|
||||
const fetchImpl = async (url, request = {}) => {
|
||||
const payload = request.body ? JSON.parse(request.body) : {};
|
||||
if (String(url).includes('/api/public/emailList')) {
|
||||
listCalls.push(payload.toEmail || '');
|
||||
return {
|
||||
ok: true,
|
||||
text: async () => JSON.stringify({ code: 200, data: { records: messages } }),
|
||||
};
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
text: async () => JSON.stringify({ code: 200, data: { token: 'token' } }),
|
||||
};
|
||||
};
|
||||
|
||||
const api = globalThis.MultiPageBackgroundCloudMailProvider.createCloudMailProvider({
|
||||
addLog: async (message, level) => logs.push({ message, level }),
|
||||
buildCloudMailHeaders: () => ({}),
|
||||
CLOUD_MAIL_DEFAULT_PAGE_SIZE: 20,
|
||||
CLOUD_MAIL_GENERATOR: 'cloudmail',
|
||||
CLOUD_MAIL_PROVIDER: 'cloudmail',
|
||||
fetchImpl,
|
||||
getCloudMailTokenFromResponse: () => 'token',
|
||||
getState: async () => ({}),
|
||||
joinCloudMailUrl: (baseUrl, path) => `${baseUrl}${path}`,
|
||||
normalizeCloudMailAddress: (value) => String(value || '').trim().toLowerCase(),
|
||||
normalizeCloudMailBaseUrl: (value) => String(value || '').trim(),
|
||||
normalizeCloudMailDomain: (value) => String(value || '').trim(),
|
||||
normalizeCloudMailDomains: (values) => values || [],
|
||||
normalizeCloudMailMailApiMessages: (payload) => payload?.data?.records || [],
|
||||
pickVerificationMessageWithTimeFallback: (currentMessages) => ({
|
||||
match: currentMessages[0]
|
||||
? {
|
||||
code: String(currentMessages[0].bodyPreview).match(/(\d{6})/)[1],
|
||||
receivedAt: Date.parse(currentMessages[0].receivedDateTime),
|
||||
message: currentMessages[0],
|
||||
}
|
||||
: null,
|
||||
usedRelaxedFilters: false,
|
||||
usedTimeFallback: false,
|
||||
}),
|
||||
setEmailState: async () => {},
|
||||
setPersistentSettings: async () => {},
|
||||
sleepWithStop: async () => {},
|
||||
throwIfStopped: () => {},
|
||||
});
|
||||
|
||||
return {
|
||||
...api,
|
||||
snapshot() {
|
||||
return { logs, listCalls };
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
test('pollCloudMailVerificationCode returns code for generated Cloud Mail address', async () => {
|
||||
const api = createProviderApi();
|
||||
|
||||
const result = await api.pollCloudMailVerificationCode(4, {
|
||||
email: 'user@example.com',
|
||||
cloudMailBaseUrl: 'https://mail.example.com',
|
||||
cloudMailAdminEmail: 'admin@example.com',
|
||||
cloudMailAdminPassword: 'secret',
|
||||
cloudMailToken: 'token',
|
||||
}, {
|
||||
targetEmail: 'user@example.com',
|
||||
maxAttempts: 1,
|
||||
intervalMs: 1,
|
||||
});
|
||||
|
||||
assert.equal(result.code, '123456');
|
||||
assert.deepEqual(api.snapshot().listCalls, ['user@example.com']);
|
||||
});
|
||||
|
||||
test('pollCloudMailVerificationCode prefers configured receive mailbox over registration email', async () => {
|
||||
const api = createProviderApi({
|
||||
receiveMailbox: 'forward-box@example.com',
|
||||
messages: [{
|
||||
id: 'mail-2',
|
||||
address: 'forward-box@example.com',
|
||||
receivedDateTime: '2026-05-07T10:20:00.000Z',
|
||||
subject: 'Login verification code',
|
||||
from: { emailAddress: { address: 'noreply@tm.openai.com' } },
|
||||
bodyPreview: 'Your verification code is 654321.',
|
||||
}],
|
||||
});
|
||||
|
||||
const result = await api.pollCloudMailVerificationCode(8, {
|
||||
email: 'duck-forwarded@duck.com',
|
||||
mailProvider: 'cloudmail',
|
||||
emailGenerator: 'duck',
|
||||
cloudMailBaseUrl: 'https://mail.example.com',
|
||||
cloudMailReceiveMailbox: 'forward-box@example.com',
|
||||
cloudMailAdminEmail: 'admin@example.com',
|
||||
cloudMailAdminPassword: 'secret',
|
||||
cloudMailToken: 'token',
|
||||
}, {
|
||||
targetEmail: 'duck-forwarded@duck.com',
|
||||
maxAttempts: 1,
|
||||
intervalMs: 1,
|
||||
});
|
||||
|
||||
assert.equal(result.code, '654321');
|
||||
assert.deepEqual(api.snapshot().listCalls, ['forward-box@example.com']);
|
||||
});
|
||||
|
||||
test('pollCloudMailVerificationCode ignores stale receive mailbox when generator owns the address', async () => {
|
||||
const api = createProviderApi({
|
||||
receiveMailbox: 'forward-box@example.com',
|
||||
messages: [{
|
||||
id: 'mail-3',
|
||||
address: 'generated@example.com',
|
||||
receivedDateTime: '2026-05-07T11:20:00.000Z',
|
||||
subject: 'Signup verification code',
|
||||
from: { emailAddress: { address: 'noreply@tm.openai.com' } },
|
||||
bodyPreview: 'Your verification code is 246810.',
|
||||
}],
|
||||
});
|
||||
|
||||
const result = await api.pollCloudMailVerificationCode(4, {
|
||||
email: 'generated@example.com',
|
||||
mailProvider: 'cloudmail',
|
||||
emailGenerator: 'cloudmail',
|
||||
cloudMailBaseUrl: 'https://mail.example.com',
|
||||
cloudMailReceiveMailbox: 'forward-box@example.com',
|
||||
cloudMailAdminEmail: 'admin@example.com',
|
||||
cloudMailAdminPassword: 'secret',
|
||||
cloudMailToken: 'token',
|
||||
}, {
|
||||
targetEmail: 'generated@example.com',
|
||||
maxAttempts: 1,
|
||||
intervalMs: 1,
|
||||
});
|
||||
|
||||
assert.equal(result.code, '246810');
|
||||
assert.deepEqual(api.snapshot().listCalls, ['generated@example.com']);
|
||||
});
|
||||
@@ -0,0 +1,73 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
|
||||
const {
|
||||
buildCloudMailHeaders,
|
||||
getCloudMailTokenFromResponse,
|
||||
normalizeCloudMailBaseUrl,
|
||||
normalizeCloudMailDomain,
|
||||
normalizeCloudMailDomains,
|
||||
normalizeCloudMailMailApiMessages,
|
||||
} = require('../cloudmail-utils.js');
|
||||
|
||||
test('normalizeCloudMailBaseUrl normalizes host and preserves path', () => {
|
||||
assert.equal(
|
||||
normalizeCloudMailBaseUrl('mail.example.com/api/'),
|
||||
'https://mail.example.com/api'
|
||||
);
|
||||
assert.equal(
|
||||
normalizeCloudMailBaseUrl('http://127.0.0.1:8080'),
|
||||
'http://127.0.0.1:8080'
|
||||
);
|
||||
assert.equal(normalizeCloudMailBaseUrl('::::'), '');
|
||||
});
|
||||
|
||||
test('normalizeCloudMailDomain and domains de-duplicate valid entries', () => {
|
||||
assert.equal(normalizeCloudMailDomain('@Mail.Example.com'), 'mail.example.com');
|
||||
assert.equal(normalizeCloudMailDomain('not-a-domain'), '');
|
||||
assert.deepEqual(
|
||||
normalizeCloudMailDomains(['mail.example.com', 'MAIL.EXAMPLE.COM', 'bad-value']),
|
||||
['mail.example.com']
|
||||
);
|
||||
});
|
||||
|
||||
test('buildCloudMailHeaders includes token and content type when needed', () => {
|
||||
assert.deepEqual(
|
||||
buildCloudMailHeaders({ token: 'token-value' }, { json: true }),
|
||||
{
|
||||
Authorization: 'token-value',
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test('normalizeCloudMailMailApiMessages extracts rows from nested Cloud Mail payloads', () => {
|
||||
const messages = normalizeCloudMailMailApiMessages({
|
||||
data: {
|
||||
records: [
|
||||
{
|
||||
emailId: 'mail-1',
|
||||
toEmail: 'User@Example.com',
|
||||
sendEmail: 'noreply@tm.openai.com',
|
||||
subject: 'OpenAI verification code',
|
||||
content: '<p>Your code is <strong>654321</strong> & ready.</p>',
|
||||
createTime: '2026-05-07 10:20:00',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(messages.length, 1);
|
||||
assert.equal(messages[0].id, 'mail-1');
|
||||
assert.equal(messages[0].address, 'user@example.com');
|
||||
assert.equal(messages[0].from.emailAddress.address, 'noreply@tm.openai.com');
|
||||
assert.match(messages[0].bodyPreview, /654321/);
|
||||
assert.match(messages[0].bodyPreview, /& ready/);
|
||||
assert.equal(messages[0].receivedDateTime, '2026-05-07T10:20:00.000Z');
|
||||
});
|
||||
|
||||
test('getCloudMailTokenFromResponse supports direct and nested response shapes', () => {
|
||||
assert.equal(getCloudMailTokenFromResponse({ token: 'one' }), 'one');
|
||||
assert.equal(getCloudMailTokenFromResponse({ data: { accessToken: 'two' } }), 'two');
|
||||
});
|
||||
@@ -28,6 +28,7 @@ test('GoPay utils builds GPC queue task and balance URLs from helper endpoints',
|
||||
const api = loadGoPayUtils();
|
||||
assert.equal(api.DEFAULT_GPC_HELPER_API_URL, 'https://gpc.qlhazycoder.top');
|
||||
assert.equal(api.normalizeGpcHelperBaseUrl(''), 'https://gpc.qlhazycoder.top');
|
||||
assert.equal(api.normalizeGpcHelperBaseUrl('https://example.com/api/gp/tasks'), 'https://gpc.qlhazycoder.top');
|
||||
assert.equal(
|
||||
api.buildGpcHelperApiUrl('', '/api/checkout/start'),
|
||||
'https://gpc.qlhazycoder.top/api/checkout/start'
|
||||
|
||||
@@ -358,13 +358,14 @@ test('pickVerificationMessageWithTimeFallback can ignore afterTimestamp while ke
|
||||
|
||||
test('buildHotmailMailApiLatestUrl includes email, client id, refresh token, and mailbox', () => {
|
||||
const url = new URL(buildHotmailMailApiLatestUrl({
|
||||
apiUrl: 'https://example.com/api/mail-new',
|
||||
clientId: 'client-123',
|
||||
email: 'user@hotmail.com',
|
||||
refreshToken: 'refresh-token-xyz',
|
||||
mailbox: 'Junk',
|
||||
}));
|
||||
|
||||
assert.equal(url.origin + url.pathname, 'https://apple.882263.xyz/api/mail-new');
|
||||
assert.equal(url.origin + url.pathname, 'https://example.com/api/mail-new');
|
||||
assert.equal(url.searchParams.get('client_id'), 'client-123');
|
||||
assert.equal(url.searchParams.get('email'), 'user@hotmail.com');
|
||||
assert.equal(url.searchParams.get('refresh_token'), 'refresh-token-xyz');
|
||||
@@ -372,6 +373,13 @@ test('buildHotmailMailApiLatestUrl includes email, client id, refresh token, and
|
||||
assert.equal(url.searchParams.get('response_type'), 'json');
|
||||
});
|
||||
|
||||
test('buildHotmailMailApiLatestUrl requires an explicit api url', () => {
|
||||
assert.throws(
|
||||
() => buildHotmailMailApiLatestUrl({ email: 'user@hotmail.com' }),
|
||||
/Hotmail mail API URL is required/
|
||||
);
|
||||
});
|
||||
|
||||
test('buildHotmailMailApiLatestUrl supports custom api url and can omit response_type', () => {
|
||||
const url = new URL(buildHotmailMailApiLatestUrl({
|
||||
apiUrl: 'https://example.com/custom-mail-api',
|
||||
|
||||
@@ -135,14 +135,18 @@ ${extractFunction('getPageTextSnapshot')}
|
||||
${extractFunction('isLoginPhoneUsernameKind')}
|
||||
${extractFunction('isLoginPhoneEntryPageText')}
|
||||
${extractFunction('isInsideHiddenPhoneControl')}
|
||||
${extractFunction('getLoginInputAttributeText')}
|
||||
${extractFunction('isLoginEmailLikeInput')}
|
||||
${extractFunction('summarizePhoneInputCandidate')}
|
||||
${extractFunction('isUsablePhoneInputElement')}
|
||||
${extractFunction('collectPhoneInputCandidates')}
|
||||
${extractFunction('findUsablePhoneInput')}
|
||||
${extractFunction('getLoginEmailInput')}
|
||||
${extractFunction('getLoginPhoneInput')}
|
||||
${extractFunction('isAddPhonePageReady')}
|
||||
|
||||
return {
|
||||
getLoginEmailInput,
|
||||
getLoginPhoneInput,
|
||||
isAddPhonePageReady,
|
||||
};
|
||||
@@ -168,6 +172,62 @@ test('step 7 does not mistake email entry with a phone switch action for phone i
|
||||
assert.equal(api.isAddPhonePageReady(), false);
|
||||
});
|
||||
|
||||
test('step 7 treats unified OpenAI login page as email input despite phone option text', () => {
|
||||
const api = createPhoneLoginEntryApi({
|
||||
href: 'https://auth.openai.com/log-in-or-create-account',
|
||||
pathname: '/log-in-or-create-account',
|
||||
pageText: '\u767b\u5f55\u6216\u6ce8\u518c \u7535\u5b50\u90ae\u4ef6\u5730\u5740 \u7ee7\u7eed \u4f7f\u7528\u7535\u8bdd\u53f7\u7801\u7ee7\u7eed',
|
||||
inputAttributes: { type: 'text', placeholder: '\u7535\u5b50\u90ae\u4ef6\u5730\u5740' },
|
||||
});
|
||||
|
||||
assert.ok(api.getLoginEmailInput(), 'unified login email input should be detected');
|
||||
assert.equal(api.getLoginPhoneInput(), null, 'phone option button must not turn email input into phone input');
|
||||
});
|
||||
|
||||
test('step 7 clicks the generic other-account entry before phone entry', async () => {
|
||||
const api = new Function(`
|
||||
const clicks = [];
|
||||
const genericEntry = { id: 'generic', textContent: '\\u767b\\u5f55\\u81f3\\u53e6\\u4e00\\u4e2a\\u5e10\\u6237' };
|
||||
const phoneEntry = { id: 'phone', textContent: '\\u4f7f\\u7528\\u7535\\u8bdd\\u53f7\\u7801\\u7ee7\\u7eed' };
|
||||
|
||||
function normalizeStep6Snapshot(snapshot) { return snapshot; }
|
||||
function inspectLoginAuthState() {
|
||||
return { state: 'entry_page', loginEntryTrigger: genericEntry, phoneEntryTrigger: phoneEntry };
|
||||
}
|
||||
function findLoginEntryTrigger() { return genericEntry; }
|
||||
function findLoginPhoneEntryTrigger() { return phoneEntry; }
|
||||
function isActionEnabled() { return true; }
|
||||
function getActionText(el) { return el.textContent || ''; }
|
||||
function log() {}
|
||||
async function humanPause() {}
|
||||
function simulateClick(el) { clicks.push(el.id); }
|
||||
async function waitForLoginEntryOpenTransition() { return { state: 'phone_entry_page' }; }
|
||||
async function switchFromEmailPageToPhoneLogin() { return { routed: 'switch-phone' }; }
|
||||
async function step6LoginFromEmailPage() { return { routed: 'email' }; }
|
||||
async function step6LoginFromPasswordPage() { return { routed: 'password' }; }
|
||||
async function step6LoginFromPhonePage() { return { routed: 'phone' }; }
|
||||
async function finalizeStep6VerificationReady() { return { routed: 'verification' }; }
|
||||
function createStep6OAuthConsentSuccessResult() { return { routed: 'oauth' }; }
|
||||
function createStep6AddEmailSuccessResult() { return { routed: 'add-email' }; }
|
||||
async function createStep6LoginTimeoutRecoveryTransition() { return { action: 'recoverable', result: { routed: 'recoverable' } }; }
|
||||
function createStep6RecoverableResult(reason, snapshot, options = {}) {
|
||||
return { step6Outcome: 'recoverable', reason, state: snapshot?.state, message: options.message || '' };
|
||||
}
|
||||
|
||||
${extractFunction('step6OpenLoginEntry')}
|
||||
|
||||
return { clicks, step6OpenLoginEntry };
|
||||
`)();
|
||||
|
||||
const result = await api.step6OpenLoginEntry(
|
||||
{ loginIdentifierType: 'phone', phoneNumber: '+441111111111' },
|
||||
{ state: 'entry_page', loginEntryTrigger: { id: 'generic', textContent: '\u767b\u5f55\u81f3\u53e6\u4e00\u4e2a\u5e10\u6237' }, phoneEntryTrigger: { id: 'phone', textContent: '\u4f7f\u7528\u7535\u8bdd\u53f7\u7801\u7ee7\u7eed' } }
|
||||
);
|
||||
|
||||
assert.deepStrictEqual(api.clicks, ['generic']);
|
||||
assert.equal(result.routed, 'phone');
|
||||
});
|
||||
|
||||
test('step 7 detects username text input when usernameKind is phone_number', () => {
|
||||
const api = createPhoneLoginEntryApi({
|
||||
phoneUsernameKind: true,
|
||||
|
||||
+22
-1
@@ -186,6 +186,7 @@
|
||||
- Plus 支付方式 `plusPaymentMethod`,GoPay 配置 `gopayPhone / gopayPin`,GPC helper 配置 `gopayHelperApiUrl / gopayHelperApiKey / gopayHelperPhoneNumber / gopayHelperOtpChannel / gopayHelperLocalSmsHelperEnabled / gopayHelperLocalSmsHelperUrl / gopayHelperPin`;其中 GPC API 地址固定归一为 `https://gpc.qlhazycoder.top`
|
||||
- PayPal 账号池配置 `paypalAccounts / currentPayPalAccountId`,以及供后台步骤兼容读取的 `paypalEmail / paypalPassword`
|
||||
- 邮箱 provider 配置
|
||||
- Cloud Mail / SkyMail API 配置:`cloudMailBaseUrl / cloudMailAdminEmail / cloudMailAdminPassword / cloudMailToken / cloudMailReceiveMailbox / cloudMailDomain / cloudMailDomains`
|
||||
- Hotmail 账号池
|
||||
- 2925 账号池
|
||||
- 2925 是否启用号池模式 `mail2925UseAccountPool`
|
||||
@@ -388,6 +389,7 @@ IP 代理模块在同步、切换、Change、出口探测和自动运行成功
|
||||
- 普通邮箱仍会携带 `filterAfterTimestamp` 做时间窗筛选;`2925` 在 Step 4 / Step 8 会固定使用“步骤开始时间向前回看 10 分钟”的时间窗,不再做“新旧邮件快照差集”比较,而是每次刷新后直接遍历当前列表中落在该固定时间窗内的匹配邮件。
|
||||
- 自动重新发送验证码次数现在使用 sidepanel 里的单一“验证码重发”配置;普通邮箱仍按 25 秒间隔节流,Hotmail / 2925 不走这个 25 秒间隔。Step 4 若启用先请求新验证码,会先消耗一次当前步骤的自动重发次数。
|
||||
- `LuckMail` 在 `/code` 接口暂未拿到新验证码时,会先点击一次认证页“重新发送验证码”,再等待 15 秒后继续轮询 `/code`,避免只空等不触发新邮件。
|
||||
- `Cloud Mail` 既可以作为邮箱生成器直接创建 `local@domain`,也可以作为收件 provider 轮询 `cloudMailReceiveMailbox`;当 `Mail = Cloud Mail` 且邮箱生成器不是 Cloud Mail 时,会优先轮询配置的接收邮箱,避免错误轮询注册邮箱本身。
|
||||
- 验证码提交重试上限当前为 15 次;页面明确拒绝验证码时,会在上限内继续拉取新验证码并重提。
|
||||
- `2925` 内容脚本会把每一封实际打开检测的邮件立即删除;同一验证码步骤启动后,试过的验证码会按“步骤 ID + 启动时间”隔离缓存,不会在本次步骤里重复提交;如果再次遇到相同验证码,对应邮件也会在读取后立即删除,避免后续反复打开。
|
||||
- `2925` 在 provide 模式下仍保持宽松匹配:只要邮件内容命中 ChatGPT / OpenAI 验证码过滤条件,就会尝试该邮件。
|
||||
@@ -549,7 +551,7 @@ Plus 模式通过 `plusModeEnabled` 开启,目标是在普通注册资料完
|
||||
Plus 模式可见步骤:
|
||||
|
||||
1. 第 1~5 步:沿用普通注册入口、邮箱、密码、注册验证码、资料填写链路。
|
||||
2. 第 6 步 `创建 Plus Checkout`:PayPal / GoPay 打开已登录 ChatGPT 页面,通过 `/api/auth/session` 读取 accessToken,再请求 `https://chatgpt.com/backend-api/payments/checkout` 创建 `chatgptplusplan` 的 checkout session。PayPal 使用 `DE / EUR` 与 `https://chatgpt.com/checkout/openai_ie/{checkout_session_id}`;GoPay 使用 `ID / IDR` 与 `https://chatgpt.com/checkout/openai_llc/{checkout_session_id}`;GPC helper 模式改为把 accessToken、手机号、国家区号、`otp_channel` 等提交到 `gopayHelperApiUrl` 的 `/api/gp/tasks`,并通过 `gopayHelperApiKey` 发送 `X-API-Key` 认证,创建后保存 `task_id`;当前默认 GPC API 地址为 `https://gpc.qlhazycoder.top`,旧的 `https://gpc.leftcode.xyz` 配置会兼容归一到新地址。
|
||||
2. 第 6 步 `创建 Plus Checkout`:PayPal / GoPay 打开已登录 ChatGPT 页面,通过 `/api/auth/session` 读取 accessToken,再请求 `https://chatgpt.com/backend-api/payments/checkout` 创建 `chatgptplusplan` 的 checkout session。PayPal 使用 `DE / EUR` 与 `https://chatgpt.com/checkout/openai_ie/{checkout_session_id}`;GoPay 使用 `ID / IDR` 与 `https://chatgpt.com/checkout/openai_llc/{checkout_session_id}`;GPC helper 模式改为把 accessToken、手机号、国家区号、`otp_channel` 等提交到 `gopayHelperApiUrl` 的 `/api/gp/tasks`,并通过 `gopayHelperApiKey` 发送 `X-API-Key` 认证,创建后保存 `task_id`;当前默认 GPC API 地址为 `https://gpc.qlhazycoder.top`。
|
||||
3. 第 7 步 `填写账单并提交订阅`:PayPal / GoPay 仍走 checkout 页面与 Stripe iframe 自动化;GPC helper 模式不再操作 checkout iframe,而是轮询 `/api/gp/tasks/{task_id}`,根据远端 `api_waiting_for` 依次向 `/otp` 和 `/pin` 提交验证码与 PIN。若侧栏启用本地 OTP helper,后台会轮询 `gopayHelperLocalSmsHelperUrl` 的 `/latest-otp?phone=...&consume=1` 接口,按当前 GPC 手机号读取并消费当前 OTP 通道的验证码;未开启本地 helper 时才弹出手动 OTP 输入框。仓库内置的 `scripts/gpc_sms_helper_macos.py` 是 macOS Messages/本地通知兼容读取实现,其他通道需要提供兼容的本地 `/latest-otp` 或 `/otp` 接口。
|
||||
4. 仅 PayPal / GoPay 会继续显示第 8 步:该步按当前支付方式显示为 `PayPal 登录与授权` 或 `GoPay 手机验证与授权`,底层 step key 仍为 `paypal-approve`。
|
||||
5. 第 9 步 `订阅回跳确认` 仅在 PayPal / GoPay 模式下等待授权后回跳到 ChatGPT / OpenAI 页面,页面加载完成后固定等待 1 秒。GPC helper 模式在第 7 步任务完成后会直接进入 Plus 可见第 10 步 OAuth 登录。
|
||||
@@ -656,9 +658,28 @@ Plus 模式可见步骤:
|
||||
- Duck
|
||||
- Cloudflare
|
||||
- Cloudflare Temp Email
|
||||
- Cloud Mail
|
||||
- 自定义邮箱池
|
||||
- iCloud 隐私邮箱
|
||||
|
||||
### 7.1.1 Cloud Mail
|
||||
|
||||
组成:
|
||||
|
||||
- [cloudmail-utils.js](c:/Users/projectf/Downloads/codex注册扩展/cloudmail-utils.js)
|
||||
- [background/cloudmail-provider.js](c:/Users/projectf/Downloads/codex注册扩展/background/cloudmail-provider.js)
|
||||
- [background/verification-flow.js](c:/Users/projectf/Downloads/codex注册扩展/background/verification-flow.js)
|
||||
- [sidepanel/sidepanel.html](c:/Users/projectf/Downloads/codex注册扩展/sidepanel/sidepanel.html)
|
||||
- [sidepanel/sidepanel.js](c:/Users/projectf/Downloads/codex注册扩展/sidepanel/sidepanel.js)
|
||||
|
||||
链路:
|
||||
|
||||
1. 侧栏展示 Cloud Mail 的 API 地址、管理员邮箱、管理员密码、接收邮箱与生成域名配置。
|
||||
2. 当 `邮箱生成 = Cloud Mail` 时,后台用管理员账号获取 Token,并通过 `/api/public/addUser` 创建随机本地名前缀邮箱。
|
||||
3. 当 `Mail = Cloud Mail` 且邮箱生成器不是 Cloud Mail 时,Step 4 / Step 8 优先轮询 `cloudMailReceiveMailbox`,用于接住 Duck / Cloudflare / iCloud 等转发来的验证码。
|
||||
4. 邮件读取统一走 `/api/public/emailList`,返回数据先经 `cloudmail-utils.js` 归一化,再复用现有验证码匹配与时间窗过滤逻辑。
|
||||
5. 如果 Token 失效,后台会尝试重新登录刷新 Token 后重试当前 API 请求。
|
||||
|
||||
### 7.2 Hotmail
|
||||
|
||||
组成:
|
||||
|
||||
+7
-3
@@ -22,6 +22,7 @@
|
||||
- `LICENSE`:项目许可证文件。
|
||||
- `README.md`:面向使用者的项目介绍、安装说明、能力清单与操作指引。
|
||||
- `background.js`:扩展后台 Service Worker 入口壳,负责模块装配、初始化、全局常量、少量保留的领域函数与运行入口;当前新增 `oauthFlowTimeoutEnabled` 持久化配置,并统一承接 OAuth 授权后链总预算开关与剩余预算计算。
|
||||
- `cloudmail-utils.js`:Cloud Mail / SkyMail 相关的纯工具函数,负责 API 地址、域名、鉴权头、邮件列表响应与邮件正文归一化。
|
||||
- `cloudflare-temp-email-utils.js`:Cloudflare Temp Email 相关的纯工具函数,负责 URL、域名、邮件内容与 MIME 数据归一化。
|
||||
- `gopay-utils.js`:GoPay / GPC Plus 支付相关纯工具函数,负责支付方式归一化、GoPay 手机号/OTP/PIN 归一化、GPC API 地址归一化、API Key 请求头、任务创建/查询/OTP/PIN/停止 URL 与余额响应解析。
|
||||
- `hotmail-utils.js`:Hotmail 账号与验证码提取相关的纯工具函数,负责账号筛选、验证码匹配、第三方接口数据归一化。
|
||||
@@ -47,6 +48,7 @@
|
||||
- `background/account-run-history.js`:账号记录模块,负责以统一账号标识维护最新记录;邮箱注册以邮箱为主身份,手机号注册以手机号为主身份,同一轮后续绑定的手机号或邮箱会并入同一条记录并覆盖旧占位状态;模块统一归一化成功/失败/停止三态并落地到 `chrome.storage.local`,兼容 phone-only 与 email+phone 组合记录、空密码,并按默认本地 helper 地址自动尝试把完整快照同步到本地 helper;若 helper 未启动,则静默跳过。
|
||||
- `background/auto-run-controller.js`:自动运行主控制器,封装多轮执行、重试、轮次摘要、线程间隔与倒计时恢复逻辑;当前自动流程会绑定 `autoRunSessionId`,手动停止后旧的倒计时计划、旧重试链路和旧恢复入口不会再复活已失效的自动运行;fresh-attempt reset 时会额外保留 Plus 模式与 PayPal 配置、`gmailBaseEmail`、`mail2925BaseEmail` 与当前 2925 账号选择,避免自动流程重置后丢失关键持久配置。
|
||||
- `background/contribution-oauth.js`:贡献模式的公开 OAuth 流程模块,负责调用 `apikey.qzz.io` 的公开贡献接口、保存贡献会话运行态、在主 10 步流程里为步骤 7 提供贡献登录地址、在步骤 9/10 衔接 callback 捕获与兼容提交 `/oauth/api/submit-callback`,并把真实服务端状态映射回 sidepanel 运行态。
|
||||
- `background/cloudmail-provider.js`:Cloud Mail / SkyMail 后台 provider 模块,负责 Token 获取、邮箱创建、邮件列表读取、验证码轮询和生成/转发收件目标邮箱选择。
|
||||
- `background/generated-email-helpers.js`:生成邮箱辅助层,除了 Duck / Cloudflare / iCloud / Cloudflare Temp Email,也统一承接 Gmail / 2925 的别名邮箱生成入口与自定义邮箱池读取;当 provider 为 2925 且 `mail2925Mode = provide` 时,会先确保当前账号池里已有可用账号,再基于该账号生成别名邮箱;若 `mail2925Mode = receive`,则会回退到普通邮箱生成链路;当生成方式为 iCloud 且 `icloudFetchMode = always_new` 时,会强制跳过未用别名复用并新建别名;当生成方式为 `custom-pool` 时,会按当前目标轮次读取邮箱池中的对应邮箱。
|
||||
- `background/ip-proxy-core.js`:IP 代理核心模块,负责代理条目解析、账号/接口代理池运行态、PAC 应用与清除、代理鉴权回填、出口探测、失败时的目标站点 fail-close 防漏规则,以及自动运行成功阈值后的代理切换支撑。
|
||||
- `background/ip-proxy-provider-711proxy.js`:711Proxy provider 规则模块,负责从账号串中识别和写回 `region / session / sessTime` 等参数,并在固定账号模式下把侧栏配置转换为最终生效的代理账号。
|
||||
@@ -59,7 +61,7 @@
|
||||
- `background/panel-bridge.js`:来源桥接层;CPA / SUB2API 继续封装页面打开、脚本注入和通信,Codex2API 则直接通过后台协议生成 OAuth 地址。
|
||||
- `background/signup-flow-helpers.js`:注册页辅助层,负责打开注册入口、等待密码页以及解析当前流程所用邮箱;在 Gmail 与 `2925 + provide` 模式下会优先复用已经存在且仍兼容的完整注册邮箱,只有不兼容或为空时才重新生成;当 provider 为 2925 且启用了 provide 模式下的号池时,会先确保账号池中已选中可用账号。
|
||||
- `background/tab-runtime.js`:标签页与内容脚本运行时基础设施,封装标签注册、冲突清理、消息超时、注入重试与队列;内容脚本恢复等待日志支持 `logStep / logStepKey`,用于保持 Plus 复用步骤的日志标签正确;当前等待标签完成、等待标签完成并短暂稳定、注入后的短暂延迟和内容脚本重试等待都已做 Stop 感知,避免用户停止后后台还继续等待并恢复执行。
|
||||
- `background/verification-flow.js`:注册/登录验证码共享流程层,封装重发、轮询、提交、失败回退、自定义邮箱跳过、共享验证码自动重发次数配置以及 2925 长轮询参数;日志步骤号使用 `completionStep`,因此 Plus 登录验证码会显示可见第 11 步而不是内部复用的 Step 8;当前验证码提交重试上限为 15 次,2925 每次重发验证码之间都会固定跑完一轮 15 次邮箱刷新轮询;若 `mail2925Mode = receive`,会额外把目标注册邮箱传给 2925 内容脚本做弱匹配;若登录验证码提交后页面转入 `add-phone / 手机号页`,则会把“后续需要手机号验证”的结果继续传给 OAuth 确认步骤,而不是误判为普通失败;若登录验证码提交后通信中断但认证页已进入 OAuth 同意页或手机号验证页,会按真实页面状态继续后续流程;若 2925 轮询命中“子邮箱已达上限邮箱”,会转入 2925 会话模块执行“记录时间、禁用 24 小时、切下一个账号并重新登录”,然后直接结束当前尝试。
|
||||
- `background/verification-flow.js`:注册/登录验证码共享流程层,封装重发、轮询、提交、失败回退、自定义邮箱跳过、共享验证码自动重发次数配置、Cloud Mail API 轮询以及 2925 长轮询参数;日志步骤号使用 `completionStep`,因此 Plus 登录验证码会显示可见第 11 步而不是内部复用的 Step 8;当前验证码提交重试上限为 15 次,2925 每次重发验证码之间都会固定跑完一轮 15 次邮箱刷新轮询;若 `mail2925Mode = receive`,会额外把目标注册邮箱传给 2925 内容脚本做弱匹配;若登录验证码提交后页面转入 `add-phone / 手机号页`,则会把“后续需要手机号验证”的结果继续传给 OAuth 确认步骤,而不是误判为普通失败;若登录验证码提交后通信中断但认证页已进入 OAuth 同意页或手机号验证页,会按真实页面状态继续后续流程;若 2925 轮询命中“子邮箱已达上限邮箱”,会转入 2925 会话模块执行“记录时间、禁用 24 小时、切下一个账号并重新登录”,然后直接结束当前尝试。
|
||||
|
||||
## `background/steps/`
|
||||
|
||||
@@ -150,7 +152,7 @@
|
||||
名基邮箱”;当 provider 为 2925 时,会额外显示 `提供邮箱 / 接收邮箱` 模式切换,并把“2925 号池”从别名基邮箱行拆成独立配置行,避免
|
||||
receive 模式把账号池开关一起隐藏;当前在 `邮箱生成` 区域新增 `自定义邮箱池` 选项和多行邮箱池输入框,并在 `邮箱服务 = 自定义邮箱` 时
|
||||
额外显示 `自定义号池` 文本框;当邮箱服务为 iCloud 时,额外提供目标邮箱类型与转发邮箱 provider 配置,用于选择直接从 iCloud 收件箱收
|
||||
码或从 QQ / 网易 / Gmail 转发目标邮箱收码;来源下拉框当前支持 `CPA / SUB2API / Codex2API`,其中 Codex2API 额外提供后台地址和管理密
|
||||
码或从 QQ / 网易 / Gmail 转发目标邮箱收码;当邮箱服务或邮箱生成器为 Cloud Mail 时,额外显示 API 地址、管理员账号、接收邮箱和生成域名配置行;来源下拉框当前支持 `CPA / SUB2API / Codex2API`,其中 Codex2API 额外提供后台地址和管理密
|
||||
钥配置行;设置卡片新增 `IP代理` 开关与代理配置折叠区,支持 711Proxy 账号密码模式、代理状态卡与出口检测按钮;接码卡内把注册方式、接码配置、接码订单运行态与“注册手机号”身份运行态分层展示,“注册手机号”位于订单运行状态下方,不嵌入当前分配/验证码网格;设置卡片新增 `Plus 模式` 开关与 PayPal 账号下拉框,右侧使用公共表单弹窗添加账号;GPC Plus 配置额外提供只读 API 地址、API Key、专用手机号、OTP 渠道、本地 OTP helper 开关、helper URL 与 PIN,并提供 `购买卡密` 与 `转换 API Key` 入口;Hotmail / 2925 两个账号池当前都使用统一的头部“添加账号/取消添加”按钮和共享表单容器;设置卡片还新增“授权总超时”开关,用于控制 Step 7 后链 5 分钟总预算。
|
||||
- `sidepanel/paypal-manager.js`:侧边栏 PayPal 账号管理器,负责 Plus 模式下的账号下拉框渲染、添加账号弹窗、保存账号与切换当前账号。
|
||||
- `sidepanel/sidepanel.js`:侧边栏主入口脚本,负责 UI 状态同步、动态步骤渲染、按钮交互、共享验证码自动重发次数配置与广播接收,并装
|
||||
@@ -158,7 +160,7 @@
|
||||
动逻辑,而独立 manager 负责贡献运行态展示与轮询,同时把 Gmail / 2925 的基邮箱输入、完整注册邮箱输入、自动生成按钮与兼容性校验统一接
|
||||
到共享别名逻辑上;当 provider 为 2925 时,会根据 `mail2925Mode` 决定是否启用别名基邮箱链路,而 2925 号池开关与当前账号选择则独立显
|
||||
示并同时服务于 provide / receive 两种模式;当 provider 为 iCloud 时,会保存并回显目标邮箱类型与转发邮箱 provider,相关 provider 选
|
||||
项和 label 复用 `mail-provider-utils.js`;`自定义邮箱池` 模式会按邮箱池长度锁定自动轮数,并把当前输入的邮箱池配置参与自动启动前保
|
||||
项和 label 复用 `mail-provider-utils.js`;Cloud Mail 配置保存、回显和显隐由这里接入,可分别服务于邮箱生成器和转发收件 provider;`自定义邮箱池` 模式会按邮箱池长度锁定自动轮数,并把当前输入的邮箱池配置参与自动启动前保
|
||||
存;点击“自动”时会先冻结当时的目标轮数,避免启动前的异步刷新、保存或后台旧状态回灌把输入框重置为 1;`邮箱服务 = 自定义邮箱` 时,如果配置了 `customMailProviderPool`,也会按号池长度锁定自动轮数并在 Auto 中按轮次分配注册邮箱,同时
|
||||
在普通失败时继续复用当前邮箱,只有成功或出现手机号验证时才切换下一个邮箱;Plus 模式开启后,步骤列表会切换为 13 步,并显示 PayPal 账
|
||||
号下拉框与添加入口;当前选中的 PayPal 账号会同步回兼容字段 `paypalEmail / paypalPassword` 供后台步骤复用;Step 8 的自定义邮箱确认弹
|
||||
@@ -219,6 +221,8 @@
|
||||
- `tests/plus-checkout-billing-tab-resolution.test.js`:测试 Plus 第 7 步可直接接管当前 checkout 标签页,并把 PayPal、账单地址、Google 地址推荐和订阅按钮操作路由到对应 Stripe iframe;当前也覆盖 GPC 队列任务轮询、本地 OTP 自动读取、手动 OTP 输入、PIN 提交与任务终止分支。
|
||||
- `tests/cloudflare-temp-email-provider.test.js`:测试 Cloudflare Temp Email provider 的轮询与目标邮箱选择逻辑。
|
||||
- `tests/cloudflare-temp-email-utils.test.js`:测试 Cloudflare Temp Email 工具层的 URL、域名、邮件解析逻辑。
|
||||
- `tests/cloudmail-provider.test.js`:测试 Cloud Mail provider 的验证码轮询和生成/转发收件目标邮箱选择逻辑。
|
||||
- `tests/cloudmail-utils.test.js`:测试 Cloud Mail 工具层的 URL、域名、鉴权头、Token 与邮件响应归一化逻辑。
|
||||
- `tests/content-utils.test.js`:测试内容脚本公共工具层对 `mail.126.com` 等网页邮箱来源的识别逻辑。
|
||||
- `tests/hotmail-api-mode.test.js`:测试 Hotmail API 模式相关文案和集成方式。
|
||||
- `tests/hotmail-cors-headers.test.js`:测试 Microsoft token 请求相关 DNR 配置。
|
||||
|
||||
Reference in New Issue
Block a user