feat: add hotmail mail api account pool flow
This commit is contained in:
+650
-50
@@ -1,9 +1,30 @@
|
||||
// background.js — Service Worker: orchestration, state, tab management, message routing
|
||||
|
||||
importScripts('data/names.js');
|
||||
importScripts('data/names.js', 'hotmail-utils.js', 'content/activation-utils.js');
|
||||
|
||||
const {
|
||||
buildHotmailMailApiLatestUrl,
|
||||
extractVerificationCodeFromMessage,
|
||||
filterHotmailAccountsByUsage,
|
||||
getLatestHotmailMessage,
|
||||
getHotmailMailApiRequestConfig,
|
||||
getHotmailVerificationPollConfig,
|
||||
getHotmailVerificationRequestTimestamp,
|
||||
normalizeHotmailMailApiMessages,
|
||||
pickHotmailAccountForRun,
|
||||
pickVerificationMessage,
|
||||
pickVerificationMessageWithFallback,
|
||||
pickVerificationMessageWithTimeFallback,
|
||||
shouldClearHotmailCurrentSelection,
|
||||
} = self.HotmailUtils;
|
||||
const {
|
||||
isRecoverableStep9AuthFailure,
|
||||
} = self.MultiPageActivationUtils;
|
||||
|
||||
const LOG_PREFIX = '[MultiPage:bg]';
|
||||
const DUCK_AUTOFILL_URL = 'https://duckduckgo.com/email/settings/autofill';
|
||||
const HOTMAIL_PROVIDER = 'hotmail-api';
|
||||
const HOTMAIL_MAILBOXES = ['INBOX', 'Junk'];
|
||||
const STOP_ERROR_MESSAGE = '流程已被用户停止。';
|
||||
const HUMAN_STEP_DELAY_MIN = 700;
|
||||
const HUMAN_STEP_DELAY_MAX = 2200;
|
||||
@@ -23,6 +44,7 @@ const PERSISTED_SETTING_DEFAULTS = {
|
||||
mailProvider: '163',
|
||||
inbucketHost: '',
|
||||
inbucketMailbox: '',
|
||||
hotmailAccounts: [],
|
||||
};
|
||||
|
||||
const PERSISTED_SETTING_KEYS = Object.keys(PERSISTED_SETTING_DEFAULTS);
|
||||
@@ -38,10 +60,13 @@ const DEFAULT_STATE = {
|
||||
password: null,
|
||||
accounts: [], // { email, password, createdAt }
|
||||
lastEmailTimestamp: null,
|
||||
signupVerificationRequestedAt: null,
|
||||
loginVerificationRequestedAt: null,
|
||||
lastSignupCode: null,
|
||||
lastLoginCode: null,
|
||||
localhostUrl: null,
|
||||
flowStartTime: null,
|
||||
currentHotmailAccountId: null,
|
||||
tabRegistry: {},
|
||||
sourceLastUrls: {},
|
||||
logs: [],
|
||||
@@ -59,6 +84,7 @@ async function getPersistedSettings() {
|
||||
...PERSISTED_SETTING_DEFAULTS,
|
||||
...stored,
|
||||
autoRunSkipFailures: Boolean(stored.autoRunSkipFailures ?? PERSISTED_SETTING_DEFAULTS.autoRunSkipFailures),
|
||||
hotmailAccounts: normalizeHotmailAccounts(stored.hotmailAccounts),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -92,9 +118,13 @@ async function setPersistentSettings(updates) {
|
||||
const persistedUpdates = {};
|
||||
for (const key of PERSISTED_SETTING_KEYS) {
|
||||
if (updates[key] !== undefined) {
|
||||
persistedUpdates[key] = key === 'autoRunSkipFailures'
|
||||
? Boolean(updates[key])
|
||||
: updates[key];
|
||||
if (key === 'autoRunSkipFailures') {
|
||||
persistedUpdates[key] = Boolean(updates[key]);
|
||||
} else if (key === 'hotmailAccounts') {
|
||||
persistedUpdates[key] = normalizeHotmailAccounts(updates[key]);
|
||||
} else {
|
||||
persistedUpdates[key] = updates[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,6 +204,408 @@ function generatePassword() {
|
||||
return pw.split('').sort(() => Math.random() - 0.5).join('');
|
||||
}
|
||||
|
||||
function normalizeHotmailAccount(account = {}) {
|
||||
return {
|
||||
id: String(account.id || crypto.randomUUID()),
|
||||
email: String(account.email || '').trim(),
|
||||
password: String(account.password || ''),
|
||||
clientId: String(account.clientId || '').trim(),
|
||||
accessToken: String(account.accessToken || ''),
|
||||
refreshToken: String(account.refreshToken || ''),
|
||||
expiresAt: Number.isFinite(Number(account.expiresAt)) ? Number(account.expiresAt) : 0,
|
||||
status: String(account.status || (account.refreshToken ? 'authorized' : 'pending')),
|
||||
enabled: account.enabled !== undefined ? Boolean(account.enabled) : true,
|
||||
used: Boolean(account.used),
|
||||
lastUsedAt: Number.isFinite(Number(account.lastUsedAt)) ? Number(account.lastUsedAt) : 0,
|
||||
lastAuthAt: Number.isFinite(Number(account.lastAuthAt)) ? Number(account.lastAuthAt) : 0,
|
||||
lastError: String(account.lastError || ''),
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeHotmailAccounts(accounts) {
|
||||
if (!Array.isArray(accounts)) return [];
|
||||
|
||||
const deduped = new Map();
|
||||
for (const account of accounts) {
|
||||
const normalized = normalizeHotmailAccount(account);
|
||||
if (!normalized.email && !normalized.id) continue;
|
||||
deduped.set(normalized.id, normalized);
|
||||
}
|
||||
return [...deduped.values()];
|
||||
}
|
||||
|
||||
function findHotmailAccount(accounts, accountId) {
|
||||
return normalizeHotmailAccounts(accounts).find((account) => account.id === accountId) || null;
|
||||
}
|
||||
|
||||
function isHotmailProvider(stateOrProvider) {
|
||||
const provider = typeof stateOrProvider === 'string'
|
||||
? stateOrProvider
|
||||
: stateOrProvider?.mailProvider;
|
||||
return provider === HOTMAIL_PROVIDER;
|
||||
}
|
||||
|
||||
async function syncHotmailAccounts(accounts) {
|
||||
const normalized = normalizeHotmailAccounts(accounts);
|
||||
await setPersistentSettings({ hotmailAccounts: normalized });
|
||||
await setState({ hotmailAccounts: normalized });
|
||||
broadcastDataUpdate({ hotmailAccounts: normalized });
|
||||
return normalized;
|
||||
}
|
||||
|
||||
async function upsertHotmailAccount(input) {
|
||||
const state = await getState();
|
||||
const accounts = normalizeHotmailAccounts(state.hotmailAccounts);
|
||||
const normalizedEmail = String(input?.email || '').trim().toLowerCase();
|
||||
const existing = input?.id
|
||||
? findHotmailAccount(accounts, input.id)
|
||||
: accounts.find((account) => account.email.toLowerCase() === normalizedEmail) || null;
|
||||
const normalized = normalizeHotmailAccount({
|
||||
...(existing || {}),
|
||||
...input,
|
||||
id: input?.id || existing?.id || crypto.randomUUID(),
|
||||
});
|
||||
|
||||
const nextAccounts = existing
|
||||
? accounts.map((account) => (account.id === normalized.id ? normalized : account))
|
||||
: [...accounts, normalized];
|
||||
|
||||
await syncHotmailAccounts(nextAccounts);
|
||||
return normalized;
|
||||
}
|
||||
|
||||
async function deleteHotmailAccount(accountId) {
|
||||
const state = await getState();
|
||||
const accounts = normalizeHotmailAccounts(state.hotmailAccounts);
|
||||
const nextAccounts = accounts.filter((account) => account.id !== accountId);
|
||||
await syncHotmailAccounts(nextAccounts);
|
||||
|
||||
if (state.currentHotmailAccountId === accountId) {
|
||||
await setState({ currentHotmailAccountId: null });
|
||||
if (isHotmailProvider(state)) {
|
||||
await setEmailState(null);
|
||||
}
|
||||
broadcastDataUpdate({ currentHotmailAccountId: null });
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteHotmailAccounts(mode = 'all') {
|
||||
const state = await getState();
|
||||
const accounts = normalizeHotmailAccounts(state.hotmailAccounts);
|
||||
const targets = filterHotmailAccountsByUsage(accounts, mode);
|
||||
const targetIds = new Set(targets.map((account) => account.id));
|
||||
const nextAccounts = mode === 'used'
|
||||
? accounts.filter((account) => !targetIds.has(account.id))
|
||||
: [];
|
||||
|
||||
await syncHotmailAccounts(nextAccounts);
|
||||
|
||||
if (state.currentHotmailAccountId && targetIds.has(state.currentHotmailAccountId)) {
|
||||
await setState({ currentHotmailAccountId: null });
|
||||
if (isHotmailProvider(state)) {
|
||||
await setEmailState(null);
|
||||
}
|
||||
broadcastDataUpdate({ currentHotmailAccountId: null });
|
||||
}
|
||||
|
||||
return {
|
||||
deletedCount: targets.length,
|
||||
remainingCount: nextAccounts.length,
|
||||
};
|
||||
}
|
||||
|
||||
async function patchHotmailAccount(accountId, updates = {}) {
|
||||
const state = await getState();
|
||||
const accounts = normalizeHotmailAccounts(state.hotmailAccounts);
|
||||
const account = findHotmailAccount(accounts, accountId);
|
||||
if (!account) {
|
||||
throw new Error('未找到对应的 Hotmail 账号。');
|
||||
}
|
||||
|
||||
const nextAccount = normalizeHotmailAccount({
|
||||
...account,
|
||||
...updates,
|
||||
id: account.id,
|
||||
});
|
||||
|
||||
await syncHotmailAccounts(accounts.map((item) => (item.id === account.id ? nextAccount : item)));
|
||||
|
||||
if (state.currentHotmailAccountId === account.id && shouldClearHotmailCurrentSelection(nextAccount)) {
|
||||
await setState({ currentHotmailAccountId: null });
|
||||
broadcastDataUpdate({ currentHotmailAccountId: null });
|
||||
if (isHotmailProvider(state)) {
|
||||
await setEmailState(null);
|
||||
}
|
||||
}
|
||||
|
||||
return nextAccount;
|
||||
}
|
||||
|
||||
async function setCurrentHotmailAccount(accountId, options = {}) {
|
||||
const { markUsed = false, syncEmail = true } = options;
|
||||
const state = await getState();
|
||||
const accounts = normalizeHotmailAccounts(state.hotmailAccounts);
|
||||
const account = findHotmailAccount(accounts, accountId);
|
||||
if (!account) {
|
||||
throw new Error('未找到对应的 Hotmail 账号。');
|
||||
}
|
||||
|
||||
if (markUsed) {
|
||||
account.lastUsedAt = Date.now();
|
||||
await syncHotmailAccounts(accounts.map((item) => (item.id === account.id ? account : item)));
|
||||
}
|
||||
|
||||
await setState({ currentHotmailAccountId: account.id });
|
||||
broadcastDataUpdate({ currentHotmailAccountId: account.id });
|
||||
if (syncEmail) {
|
||||
await setEmailState(account.email || null);
|
||||
}
|
||||
return account;
|
||||
}
|
||||
|
||||
async function ensureHotmailAccountForFlow(options = {}) {
|
||||
const { allowAllocate = true, markUsed = false, preferredAccountId = null } = options;
|
||||
const state = await getState();
|
||||
const accounts = normalizeHotmailAccounts(state.hotmailAccounts);
|
||||
const isAccountAllocatable = (candidate) => Boolean(candidate)
|
||||
&& candidate.status === 'authorized'
|
||||
&& !candidate.used
|
||||
&& Boolean(candidate.refreshToken);
|
||||
|
||||
let account = null;
|
||||
if (preferredAccountId) {
|
||||
account = findHotmailAccount(accounts, preferredAccountId);
|
||||
}
|
||||
if (!account && state.currentHotmailAccountId) {
|
||||
account = findHotmailAccount(accounts, state.currentHotmailAccountId);
|
||||
}
|
||||
if ((!account || !isAccountAllocatable(account)) && allowAllocate) {
|
||||
account = pickHotmailAccountForRun(accounts, {});
|
||||
}
|
||||
|
||||
if (!account) {
|
||||
throw new Error('没有可用的 Hotmail 账号。请先在侧边栏添加至少一个带 refresh token 的账号。');
|
||||
}
|
||||
if (!isAccountAllocatable(account)) {
|
||||
throw new Error(`Hotmail 账号 ${account.email || account.id} 尚未就绪,无法读取邮件。`);
|
||||
}
|
||||
|
||||
return setCurrentHotmailAccount(account.id, { markUsed, syncEmail: true });
|
||||
}
|
||||
|
||||
async function requestHotmailMailApi(account, mailbox = 'INBOX') {
|
||||
if (!account?.email) {
|
||||
throw new Error('Hotmail 账号缺少邮箱地址。');
|
||||
}
|
||||
if (!account?.clientId) {
|
||||
throw new Error(`Hotmail 账号 ${account.email || account.id} 缺少 client ID。`);
|
||||
}
|
||||
if (!account?.refreshToken) {
|
||||
throw new Error(`Hotmail 账号 ${account.email || account.id} 缺少 refresh token。`);
|
||||
}
|
||||
|
||||
const url = buildHotmailMailApiLatestUrl({
|
||||
clientId: account.clientId,
|
||||
email: account.email,
|
||||
refreshToken: account.refreshToken,
|
||||
mailbox,
|
||||
responseType: 'json',
|
||||
});
|
||||
const { timeoutMs } = getHotmailMailApiRequestConfig();
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(new Error('timeout')), timeoutMs);
|
||||
|
||||
let response;
|
||||
try {
|
||||
response = await fetch(url, { method: 'GET', signal: controller.signal });
|
||||
} catch (err) {
|
||||
if (err?.name === 'AbortError') {
|
||||
throw new Error(`Hotmail API 请求超时(>${Math.round(timeoutMs / 1000)} 秒):${mailbox}`);
|
||||
}
|
||||
throw new Error(`Hotmail API 请求失败:${err.message}`);
|
||||
} finally {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
|
||||
const text = await response.text();
|
||||
let payload = {};
|
||||
try {
|
||||
payload = text ? JSON.parse(text) : {};
|
||||
} catch {
|
||||
payload = { raw: text };
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = payload?.message || payload?.error || payload?.msg || text || `HTTP ${response.status}`;
|
||||
throw new Error(`Hotmail API 请求失败:${errorText}`);
|
||||
}
|
||||
|
||||
if (payload && payload.success === false) {
|
||||
const errorText = payload?.message || payload?.msg || payload?.error || '未知错误';
|
||||
throw new Error(`Hotmail API 返回失败:${errorText}`);
|
||||
}
|
||||
|
||||
return {
|
||||
mailbox,
|
||||
payload,
|
||||
messages: normalizeHotmailMailApiMessages(payload?.data),
|
||||
nextRefreshToken: String(payload?.new_refresh_token || payload?.newRefreshToken || '').trim(),
|
||||
};
|
||||
}
|
||||
|
||||
function applyHotmailApiResultToAccount(account, apiResult) {
|
||||
const nextRefreshToken = String(apiResult?.nextRefreshToken || '').trim();
|
||||
return {
|
||||
...account,
|
||||
accessToken: '',
|
||||
expiresAt: 0,
|
||||
refreshToken: nextRefreshToken || account.refreshToken,
|
||||
status: 'authorized',
|
||||
lastAuthAt: Date.now(),
|
||||
lastError: '',
|
||||
};
|
||||
}
|
||||
|
||||
async function fetchHotmailMailboxMessages(account, mailboxes = HOTMAIL_MAILBOXES) {
|
||||
let workingAccount = normalizeHotmailAccount(account);
|
||||
const mailboxResults = [];
|
||||
|
||||
for (const mailbox of mailboxes) {
|
||||
const result = await requestHotmailMailApi(workingAccount, mailbox);
|
||||
workingAccount = applyHotmailApiResultToAccount(workingAccount, result);
|
||||
mailboxResults.push({
|
||||
mailbox,
|
||||
count: result.messages.length,
|
||||
messages: result.messages.map((message) => ({ ...message, mailbox })),
|
||||
});
|
||||
}
|
||||
|
||||
const savedAccount = await upsertHotmailAccount(workingAccount);
|
||||
return {
|
||||
account: savedAccount,
|
||||
mailboxResults,
|
||||
messages: mailboxResults.flatMap((item) => item.messages),
|
||||
};
|
||||
}
|
||||
|
||||
async function verifyHotmailAccount(accountId) {
|
||||
const state = await getState();
|
||||
const account = findHotmailAccount(state.hotmailAccounts, accountId);
|
||||
if (!account) {
|
||||
throw new Error('未找到需要校验的 Hotmail 账号。');
|
||||
}
|
||||
|
||||
const result = await fetchHotmailMailboxMessages(account, ['INBOX']);
|
||||
return {
|
||||
account: result.account,
|
||||
messageCount: result.mailboxResults[0]?.count || 0,
|
||||
};
|
||||
}
|
||||
|
||||
async function testHotmailAccountMailAccess(accountId) {
|
||||
const state = await getState();
|
||||
const account = findHotmailAccount(state.hotmailAccounts, accountId);
|
||||
if (!account) {
|
||||
throw new Error('未找到需要测试的 Hotmail 账号。');
|
||||
}
|
||||
|
||||
const result = await fetchHotmailMailboxMessages(account, HOTMAIL_MAILBOXES);
|
||||
const latestMessage = getLatestHotmailMessage(result.messages);
|
||||
const latestCode = latestMessage ? extractVerificationCodeFromMessage(latestMessage) : null;
|
||||
|
||||
return {
|
||||
account: result.account,
|
||||
accountId: result.account.id,
|
||||
email: result.account.email,
|
||||
messageCount: result.messages.length,
|
||||
latestSubject: latestMessage?.subject || '',
|
||||
latestMailbox: latestMessage?.mailbox || '',
|
||||
latestCode: latestCode || '',
|
||||
inboxCount: result.mailboxResults.find((item) => item.mailbox === 'INBOX')?.count || 0,
|
||||
junkCount: result.mailboxResults.find((item) => item.mailbox === 'Junk')?.count || 0,
|
||||
};
|
||||
}
|
||||
|
||||
async function pollHotmailVerificationCode(step, state, pollPayload = {}) {
|
||||
await addLog(`步骤 ${step}:正在确定 Hotmail 收信账号...`, 'info');
|
||||
let account = await ensureHotmailAccountForFlow({
|
||||
allowAllocate: true,
|
||||
markUsed: false,
|
||||
preferredAccountId: state.currentHotmailAccountId || null,
|
||||
});
|
||||
await addLog(`步骤 ${step}:当前使用 Hotmail 账号 ${account.email} 轮询收件箱。`, 'info');
|
||||
|
||||
const maxAttempts = Number(pollPayload.maxAttempts) || 5;
|
||||
const intervalMs = Number(pollPayload.intervalMs) || 3000;
|
||||
let lastError = null;
|
||||
|
||||
function summarizeMessagesForLog(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 || 'unknown-time';
|
||||
const sender = message?.from?.emailAddress?.address || 'unknown';
|
||||
const subject = message?.subject || '(no subject)';
|
||||
const preview = String(message?.bodyPreview || '').replace(/\s+/g, ' ').trim().slice(0, 80);
|
||||
return `[${message.mailbox || 'INBOX'}] ${receivedAt} | ${sender} | ${subject} | ${preview}`;
|
||||
})
|
||||
.join(' || ');
|
||||
}
|
||||
|
||||
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
||||
throwIfStopped();
|
||||
try {
|
||||
await addLog(`步骤 ${step}:正在轮询 Hotmail 邮件(${attempt}/${maxAttempts})...`, 'info');
|
||||
const fetchResult = await fetchHotmailMailboxMessages(account, HOTMAIL_MAILBOXES);
|
||||
account = fetchResult.account;
|
||||
const matchResult = pickVerificationMessageWithTimeFallback(fetchResult.messages, {
|
||||
afterTimestamp: pollPayload.filterAfterTimestamp || 0,
|
||||
senderFilters: pollPayload.senderFilters || [],
|
||||
subjectFilters: pollPayload.subjectFilters || [],
|
||||
excludeCodes: pollPayload.excludeCodes || [],
|
||||
});
|
||||
const match = matchResult.match;
|
||||
|
||||
if (match?.code) {
|
||||
const mailboxLabel = match.message?.mailbox || 'INBOX';
|
||||
if (matchResult.usedRelaxedFilters) {
|
||||
const fallbackLabel = matchResult.usedTimeFallback ? '宽松匹配 + 时间回退' : '宽松匹配';
|
||||
await addLog(`步骤 ${step}:严格规则未命中,已改用 ${fallbackLabel} 并命中 Hotmail ${mailboxLabel} 验证码。`, 'warn');
|
||||
}
|
||||
await addLog(`步骤 ${step}:已在 Hotmail ${mailboxLabel} 中找到验证码:${match.code}`, 'ok');
|
||||
return {
|
||||
ok: true,
|
||||
code: match.code,
|
||||
emailTimestamp: match.receivedAt || Date.now(),
|
||||
mailId: match.message?.id || '',
|
||||
};
|
||||
}
|
||||
|
||||
lastError = new Error(`步骤 ${step}:暂未在 Hotmail 收件箱中找到匹配验证码(${attempt}/${maxAttempts})。`);
|
||||
await addLog(lastError.message, attempt === maxAttempts ? 'warn' : 'info');
|
||||
const mailSummary = summarizeMessagesForLog(fetchResult.messages);
|
||||
if (mailSummary) {
|
||||
await addLog(`步骤 ${step}:最近邮件样本:${mailSummary}`, 'info');
|
||||
}
|
||||
} catch (err) {
|
||||
lastError = err;
|
||||
await addLog(`步骤 ${step}:Hotmail 收件箱轮询失败:${err.message}`, 'warn');
|
||||
}
|
||||
|
||||
if (attempt < maxAttempts) {
|
||||
await sleepWithStop(intervalMs);
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError || new Error(`步骤 ${step}:未在 Hotmail 收件箱中找到新的匹配验证码。`);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Tab Registry
|
||||
// ============================================================
|
||||
@@ -614,6 +1046,7 @@ function getSourceLabel(source) {
|
||||
'mail-163': '163 邮箱',
|
||||
'inbucket-mail': 'Inbucket 邮箱',
|
||||
'duck-mail': 'Duck 邮箱',
|
||||
'hotmail-api': 'Hotmail API',
|
||||
};
|
||||
return labels[source] || source || '未知来源';
|
||||
}
|
||||
@@ -650,7 +1083,7 @@ function getErrorMessage(error) {
|
||||
|
||||
function isVerificationMailPollingError(error) {
|
||||
const message = getErrorMessage(error);
|
||||
return /未在 .*邮箱中找到新的匹配邮件|邮箱轮询结束,但未获取到验证码|无法获取新的(?:注册|登录)验证码|页面未能重新就绪|页面通信异常|did not respond in \d+s/i.test(message);
|
||||
return /未在 .*邮箱中找到新的匹配邮件|未在 Hotmail 收件箱中找到新的匹配验证码|邮箱轮询结束,但未获取到验证码|无法获取新的(?:注册|登录)验证码|页面未能重新就绪|页面通信异常|did not respond in \d+s/i.test(message);
|
||||
}
|
||||
|
||||
function isRestartCurrentAttemptError(error) {
|
||||
@@ -658,7 +1091,13 @@ function isRestartCurrentAttemptError(error) {
|
||||
return /当前邮箱已存在,需要重新开始新一轮/.test(message);
|
||||
}
|
||||
|
||||
function isStep9OAuthTimeoutError(error) {
|
||||
function isStep9RecoverableAuthError(error) {
|
||||
const message = String(typeof error === 'string' ? error : error?.message || '');
|
||||
return /STEP9_OAUTH_RETRY::/i.test(message)
|
||||
|| isRecoverableStep9AuthFailure(message);
|
||||
}
|
||||
|
||||
function isLegacyStep9RecoverableAuthError(error) {
|
||||
const message = String(typeof error === 'string' ? error : error?.message || '');
|
||||
return /STEP9_OAUTH_TIMEOUT::|认证失败:\s*Timeout waiting for OAuth callback/i.test(message);
|
||||
}
|
||||
@@ -687,6 +1126,8 @@ function getDownstreamStateResets(step) {
|
||||
flowStartTime: null,
|
||||
password: null,
|
||||
lastEmailTimestamp: null,
|
||||
signupVerificationRequestedAt: null,
|
||||
loginVerificationRequestedAt: null,
|
||||
lastSignupCode: null,
|
||||
lastLoginCode: null,
|
||||
localhostUrl: null,
|
||||
@@ -696,6 +1137,8 @@ function getDownstreamStateResets(step) {
|
||||
return {
|
||||
password: null,
|
||||
lastEmailTimestamp: null,
|
||||
signupVerificationRequestedAt: null,
|
||||
loginVerificationRequestedAt: null,
|
||||
lastSignupCode: null,
|
||||
lastLoginCode: null,
|
||||
localhostUrl: null,
|
||||
@@ -704,6 +1147,8 @@ function getDownstreamStateResets(step) {
|
||||
if (step === 3 || step === 4) {
|
||||
return {
|
||||
lastEmailTimestamp: null,
|
||||
signupVerificationRequestedAt: null,
|
||||
loginVerificationRequestedAt: null,
|
||||
lastSignupCode: null,
|
||||
lastLoginCode: null,
|
||||
localhostUrl: null,
|
||||
@@ -712,6 +1157,7 @@ function getDownstreamStateResets(step) {
|
||||
if (step === 5 || step === 6 || step === 7) {
|
||||
return {
|
||||
lastLoginCode: null,
|
||||
loginVerificationRequestedAt: null,
|
||||
localhostUrl: null,
|
||||
};
|
||||
}
|
||||
@@ -1067,6 +1513,63 @@ async function handleMessage(message, sender) {
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
case 'UPSERT_HOTMAIL_ACCOUNT': {
|
||||
const account = await upsertHotmailAccount(message.payload || {});
|
||||
return { ok: true, account };
|
||||
}
|
||||
|
||||
case 'DELETE_HOTMAIL_ACCOUNT': {
|
||||
await deleteHotmailAccount(String(message.payload?.accountId || ''));
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
case 'DELETE_HOTMAIL_ACCOUNTS': {
|
||||
const result = await deleteHotmailAccounts(String(message.payload?.mode || 'all'));
|
||||
return { ok: true, ...result };
|
||||
}
|
||||
|
||||
case 'SELECT_HOTMAIL_ACCOUNT': {
|
||||
const account = await setCurrentHotmailAccount(String(message.payload?.accountId || ''), {
|
||||
markUsed: false,
|
||||
syncEmail: true,
|
||||
});
|
||||
return { ok: true, account };
|
||||
}
|
||||
|
||||
case 'PATCH_HOTMAIL_ACCOUNT': {
|
||||
const account = await patchHotmailAccount(
|
||||
String(message.payload?.accountId || ''),
|
||||
message.payload?.updates || {}
|
||||
);
|
||||
return { ok: true, account };
|
||||
}
|
||||
|
||||
case 'VERIFY_HOTMAIL_ACCOUNT':
|
||||
case 'AUTHORIZE_HOTMAIL_ACCOUNT': {
|
||||
const accountId = String(message.payload?.accountId || '');
|
||||
try {
|
||||
const result = await verifyHotmailAccount(accountId);
|
||||
await setCurrentHotmailAccount(result.account.id, { markUsed: false, syncEmail: true });
|
||||
await addLog(`Hotmail 账号 ${result.account.email} 校验通过,可直接用于收信。`, 'ok');
|
||||
return { ok: true, account: result.account, messageCount: result.messageCount };
|
||||
} catch (err) {
|
||||
const state = await getState();
|
||||
const accounts = normalizeHotmailAccounts(state.hotmailAccounts);
|
||||
const target = findHotmailAccount(accounts, accountId);
|
||||
if (target) {
|
||||
target.status = 'error';
|
||||
target.lastError = err.message;
|
||||
await syncHotmailAccounts(accounts.map((item) => (item.id === target.id ? target : item)));
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
case 'TEST_HOTMAIL_ACCOUNT': {
|
||||
const result = await testHotmailAccountMailAccess(String(message.payload?.accountId || ''));
|
||||
return { ok: true, ...result };
|
||||
}
|
||||
|
||||
// Side panel data updates
|
||||
case 'SAVE_EMAIL': {
|
||||
const state = await getState();
|
||||
@@ -1114,9 +1617,29 @@ async function handleStepData(step, payload) {
|
||||
break;
|
||||
case 3:
|
||||
if (payload.email) await setEmailState(payload.email);
|
||||
if (payload.signupVerificationRequestedAt) {
|
||||
await setState({ signupVerificationRequestedAt: payload.signupVerificationRequestedAt });
|
||||
}
|
||||
if (payload.loginVerificationRequestedAt) {
|
||||
await setState({ loginVerificationRequestedAt: payload.loginVerificationRequestedAt });
|
||||
}
|
||||
break;
|
||||
case 6:
|
||||
if (payload.loginVerificationRequestedAt) {
|
||||
await setState({ loginVerificationRequestedAt: payload.loginVerificationRequestedAt });
|
||||
}
|
||||
break;
|
||||
case 4:
|
||||
if (payload.emailTimestamp) await setState({ lastEmailTimestamp: payload.emailTimestamp });
|
||||
await setState({
|
||||
lastEmailTimestamp: payload.emailTimestamp || null,
|
||||
signupVerificationRequestedAt: null,
|
||||
});
|
||||
break;
|
||||
case 7:
|
||||
await setState({
|
||||
lastEmailTimestamp: payload.emailTimestamp || null,
|
||||
loginVerificationRequestedAt: null,
|
||||
});
|
||||
break;
|
||||
case 8:
|
||||
if (payload.localhostUrl) {
|
||||
@@ -1125,6 +1648,14 @@ async function handleStepData(step, payload) {
|
||||
}
|
||||
break;
|
||||
case 9: {
|
||||
const latestState = await getState();
|
||||
if (latestState.currentHotmailAccountId && isHotmailProvider(latestState)) {
|
||||
await patchHotmailAccount(latestState.currentHotmailAccountId, {
|
||||
used: true,
|
||||
lastUsedAt: Date.now(),
|
||||
});
|
||||
await addLog('当前 Hotmail 账号已自动标记为已用。', 'ok');
|
||||
}
|
||||
const localhostPrefix = buildLocalhostCleanupPrefix(payload.localhostUrl);
|
||||
if (localhostPrefix) {
|
||||
await closeTabsByUrlPrefix(localhostPrefix);
|
||||
@@ -1352,6 +1883,16 @@ async function resumeAutoRunIfWaitingForEmail(options = {}) {
|
||||
|
||||
async function ensureAutoEmailReady(targetRun, totalRuns, attemptRuns) {
|
||||
const currentState = await getState();
|
||||
if (isHotmailProvider(currentState)) {
|
||||
const account = await ensureHotmailAccountForFlow({
|
||||
allowAllocate: true,
|
||||
markUsed: true,
|
||||
preferredAccountId: null,
|
||||
});
|
||||
await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮:已分配 Hotmail 账号 ${account.email}(第 ${attemptRuns} 次尝试)===`, 'ok');
|
||||
return account.email;
|
||||
}
|
||||
|
||||
if (currentState.email) {
|
||||
return currentState.email;
|
||||
}
|
||||
@@ -1430,14 +1971,23 @@ async function runAutoSequenceFromStep(startStep, context = {}) {
|
||||
await executeStepAndWait(step, AUTO_STEP_DELAYS[step]);
|
||||
step += 1;
|
||||
} catch (err) {
|
||||
if (step === 9 && isStep9OAuthTimeoutError(err) && step9RestartAttempts < maxStep9RestartAttempts) {
|
||||
const latestState = await getState();
|
||||
const currentMail = getMailConfig(latestState);
|
||||
const shouldRetryStep9 = step === 9
|
||||
&& (
|
||||
isLegacyStep9RecoverableAuthError(err)
|
||||
|| (currentMail.provider === HOTMAIL_PROVIDER && isStep9RecoverableAuthError(err))
|
||||
)
|
||||
&& step9RestartAttempts < maxStep9RestartAttempts;
|
||||
|
||||
if (shouldRetryStep9) {
|
||||
step9RestartAttempts += 1;
|
||||
await addLog(
|
||||
`步骤 9:检测到 OAuth callback 超时,正在回到步骤 6 重新开始授权流程(${step9RestartAttempts}/${maxStep9RestartAttempts})...`,
|
||||
`步骤 9:检测到 CPA 认证失败,正在回到步骤 6 重新开始授权流程(${step9RestartAttempts}/${maxStep9RestartAttempts})...`,
|
||||
'warn'
|
||||
);
|
||||
await invalidateDownstreamAfterStepRestart(6, {
|
||||
logLabel: `步骤 9 超时后准备回到步骤 6 重试(${step9RestartAttempts}/${maxStep9RestartAttempts})`,
|
||||
logLabel: `步骤 9 认证失败后准备回到步骤 6 重试(${step9RestartAttempts}/${maxStep9RestartAttempts})`,
|
||||
});
|
||||
step = 6;
|
||||
continue;
|
||||
@@ -1696,7 +2246,7 @@ async function executeStep1(state) {
|
||||
}
|
||||
await addLog('步骤 1:正在打开 CPA 面板...');
|
||||
await reuseOrCreateTab('vps-panel', state.vpsUrl, {
|
||||
inject: ['content/utils.js', 'content/vps-panel.js'],
|
||||
inject: ['content/activation-utils.js', 'content/utils.js', 'content/vps-panel.js'],
|
||||
reloadIfSameUrl: true,
|
||||
});
|
||||
|
||||
@@ -1732,26 +2282,39 @@ async function executeStep2(state) {
|
||||
// ============================================================
|
||||
|
||||
async function executeStep3(state) {
|
||||
if (!state.email) {
|
||||
let resolvedEmail = state.email;
|
||||
if (isHotmailProvider(state)) {
|
||||
const account = await ensureHotmailAccountForFlow({
|
||||
allowAllocate: true,
|
||||
markUsed: true,
|
||||
preferredAccountId: state.currentHotmailAccountId || null,
|
||||
});
|
||||
resolvedEmail = account.email;
|
||||
}
|
||||
|
||||
if (!resolvedEmail) {
|
||||
throw new Error('缺少邮箱地址,请先在侧边栏粘贴邮箱。');
|
||||
}
|
||||
|
||||
const password = state.customPassword || generatePassword();
|
||||
if (resolvedEmail !== state.email) {
|
||||
await setEmailState(resolvedEmail);
|
||||
}
|
||||
await setPasswordState(password);
|
||||
|
||||
// Save account record
|
||||
const accounts = state.accounts || [];
|
||||
accounts.push({ email: state.email, password, createdAt: new Date().toISOString() });
|
||||
accounts.push({ email: resolvedEmail, password, createdAt: new Date().toISOString() });
|
||||
await setState({ accounts });
|
||||
|
||||
await addLog(
|
||||
`步骤 3:正在填写邮箱 ${state.email},密码为${state.customPassword ? '自定义' : '自动生成'}(${password.length} 位)`
|
||||
`步骤 3:正在填写邮箱 ${resolvedEmail},密码为${state.customPassword ? '自定义' : '自动生成'}(${password.length} 位)`
|
||||
);
|
||||
await sendToContentScript('signup-page', {
|
||||
type: 'EXECUTE_STEP',
|
||||
step: 3,
|
||||
source: 'background',
|
||||
payload: { email: state.email, password },
|
||||
payload: { email: resolvedEmail, password },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1761,6 +2324,9 @@ async function executeStep3(state) {
|
||||
|
||||
function getMailConfig(state) {
|
||||
const provider = state.mailProvider || 'qq';
|
||||
if (provider === HOTMAIL_PROVIDER) {
|
||||
return { provider: HOTMAIL_PROVIDER, label: 'Hotmail API' };
|
||||
}
|
||||
if (provider === '163') {
|
||||
return { source: 'mail-163', url: 'https://mail.163.com/js6/main.jsp?df=mail163_letter#module=mbox.ListModule%7C%7B%22fid%22%3A1%2C%22order%22%3A%22date%22%2C%22desc%22%3Atrue%7D', label: '163 邮箱' };
|
||||
}
|
||||
@@ -1778,7 +2344,7 @@ function getMailConfig(state) {
|
||||
url: `${host}/m/${encodeURIComponent(mailbox)}/`,
|
||||
label: `Inbucket 邮箱(${mailbox})`,
|
||||
navigateOnReuse: true,
|
||||
inject: ['content/utils.js', 'content/inbucket-mail.js'],
|
||||
inject: ['content/activation-utils.js', 'content/utils.js', 'content/inbucket-mail.js'],
|
||||
injectSource: 'inbucket-mail',
|
||||
};
|
||||
}
|
||||
@@ -1810,7 +2376,7 @@ function getVerificationCodeLabel(step) {
|
||||
function getVerificationPollPayload(step, state, overrides = {}) {
|
||||
if (step === 4) {
|
||||
return {
|
||||
filterAfterTimestamp: state.flowStartTime || 0,
|
||||
filterAfterTimestamp: getHotmailVerificationRequestTimestamp(4, state),
|
||||
senderFilters: ['openai', 'noreply', 'verify', 'auth', 'duckduckgo', 'forward'],
|
||||
subjectFilters: ['verify', 'verification', 'code', '楠岃瘉', 'confirm'],
|
||||
targetEmail: state.email,
|
||||
@@ -1821,7 +2387,7 @@ function getVerificationPollPayload(step, state, overrides = {}) {
|
||||
}
|
||||
|
||||
return {
|
||||
filterAfterTimestamp: state.lastEmailTimestamp || state.flowStartTime || 0,
|
||||
filterAfterTimestamp: getHotmailVerificationRequestTimestamp(7, state),
|
||||
senderFilters: ['openai', 'noreply', 'verify', 'auth', 'chatgpt', 'duckduckgo', 'forward'],
|
||||
subjectFilters: ['verify', 'verification', 'code', '楠岃瘉', 'confirm', 'login'],
|
||||
targetEmail: state.email,
|
||||
@@ -1855,6 +2421,15 @@ async function requestVerificationCodeResend(step) {
|
||||
}
|
||||
|
||||
async function pollFreshVerificationCode(step, state, mail, pollOverrides = {}) {
|
||||
if (mail.provider === HOTMAIL_PROVIDER) {
|
||||
const hotmailPollConfig = getHotmailVerificationPollConfig(step);
|
||||
return pollHotmailVerificationCode(step, state, {
|
||||
...getVerificationPollPayload(step, state),
|
||||
...hotmailPollConfig,
|
||||
...pollOverrides,
|
||||
});
|
||||
}
|
||||
|
||||
const stateKey = getVerificationCodeStateKey(step);
|
||||
const rejectedCodes = new Set();
|
||||
if (state[stateKey]) {
|
||||
@@ -1943,12 +2518,18 @@ async function submitVerificationCode(step, code) {
|
||||
async function resolveVerificationStep(step, state, mail, options = {}) {
|
||||
const stateKey = getVerificationCodeStateKey(step);
|
||||
const rejectedCodes = new Set();
|
||||
if (state[stateKey]) {
|
||||
const hotmailPollConfig = mail.provider === HOTMAIL_PROVIDER
|
||||
? getHotmailVerificationPollConfig(step)
|
||||
: null;
|
||||
const ignorePersistedLastCode = Boolean(hotmailPollConfig?.ignorePersistedLastCode);
|
||||
if (state[stateKey] && !ignorePersistedLastCode) {
|
||||
rejectedCodes.add(state[stateKey]);
|
||||
}
|
||||
|
||||
const nextFilterAfterTimestamp = options.filterAfterTimestamp ?? null;
|
||||
const requestFreshCodeFirst = Boolean(options.requestFreshCodeFirst);
|
||||
const requestFreshCodeFirst = options.requestFreshCodeFirst !== undefined
|
||||
? Boolean(options.requestFreshCodeFirst)
|
||||
: (hotmailPollConfig?.requestFreshCodeFirst ?? false);
|
||||
const maxSubmitAttempts = 3;
|
||||
|
||||
if (requestFreshCodeFirst) {
|
||||
@@ -1960,6 +2541,14 @@ async function resolveVerificationStep(step, state, mail, options = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
if (mail.provider === HOTMAIL_PROVIDER) {
|
||||
const initialDelayMs = Number(options.initialDelayMs ?? hotmailPollConfig.initialDelayMs) || 0;
|
||||
if (initialDelayMs > 0) {
|
||||
await addLog(`步骤 ${step}:等待 ${Math.round(initialDelayMs / 1000)} 秒,让 Hotmail 验证码邮件先到达...`, 'info');
|
||||
await sleepWithStop(initialDelayMs);
|
||||
}
|
||||
}
|
||||
|
||||
for (let attempt = 1; attempt <= maxSubmitAttempts; attempt++) {
|
||||
const result = await pollFreshVerificationCode(step, state, mail, {
|
||||
excludeCodes: [...rejectedCodes],
|
||||
@@ -2024,35 +2613,42 @@ async function executeStep4(state) {
|
||||
if (prepareResult && prepareResult.error) {
|
||||
throw new Error(prepareResult.error);
|
||||
}
|
||||
if (prepareResult?.verificationRequestedAt) {
|
||||
await setState({ loginVerificationRequestedAt: prepareResult.verificationRequestedAt });
|
||||
}
|
||||
if (prepareResult?.alreadyVerified) {
|
||||
await completeStepFromBackground(4, {});
|
||||
return;
|
||||
}
|
||||
|
||||
await addLog(`步骤 4:正在打开${mail.label}...`);
|
||||
if (mail.provider === HOTMAIL_PROVIDER) {
|
||||
await addLog(`步骤 4:正在通过 ${mail.label} 轮询验证码...`);
|
||||
} else {
|
||||
await addLog(`步骤 4:正在打开${mail.label}...`);
|
||||
|
||||
// For mail tabs, only create if not alive — don't navigate (preserves login session)
|
||||
const alive = await isTabAlive(mail.source);
|
||||
if (alive) {
|
||||
if (mail.navigateOnReuse) {
|
||||
// For mail tabs, only create if not alive — don't navigate (preserves login session)
|
||||
const alive = await isTabAlive(mail.source);
|
||||
if (alive) {
|
||||
if (mail.navigateOnReuse) {
|
||||
await reuseOrCreateTab(mail.source, mail.url, {
|
||||
inject: mail.inject,
|
||||
injectSource: mail.injectSource,
|
||||
});
|
||||
} else {
|
||||
const tabId = await getTabId(mail.source);
|
||||
await chrome.tabs.update(tabId, { active: true });
|
||||
}
|
||||
} else {
|
||||
await reuseOrCreateTab(mail.source, mail.url, {
|
||||
inject: mail.inject,
|
||||
injectSource: mail.injectSource,
|
||||
});
|
||||
} else {
|
||||
const tabId = await getTabId(mail.source);
|
||||
await chrome.tabs.update(tabId, { active: true });
|
||||
}
|
||||
} else {
|
||||
await reuseOrCreateTab(mail.source, mail.url, {
|
||||
inject: mail.inject,
|
||||
injectSource: mail.injectSource,
|
||||
});
|
||||
}
|
||||
|
||||
await resolveVerificationStep(4, state, mail, {
|
||||
filterAfterTimestamp: stepStartedAt,
|
||||
requestFreshCodeFirst: true,
|
||||
filterAfterTimestamp: mail.provider === HOTMAIL_PROVIDER ? undefined : stepStartedAt,
|
||||
requestFreshCodeFirst: mail.provider === HOTMAIL_PROVIDER ? false : true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -2148,29 +2744,33 @@ async function runStep7Attempt(state) {
|
||||
throw new Error(prepareResult.error);
|
||||
}
|
||||
|
||||
await addLog(`步骤 7:正在打开${mail.label}...`);
|
||||
if (mail.provider === HOTMAIL_PROVIDER) {
|
||||
await addLog(`步骤 7:正在通过 ${mail.label} 轮询验证码...`);
|
||||
} else {
|
||||
await addLog(`步骤 7:正在打开${mail.label}...`);
|
||||
|
||||
const alive = await isTabAlive(mail.source);
|
||||
if (alive) {
|
||||
if (mail.navigateOnReuse) {
|
||||
const alive = await isTabAlive(mail.source);
|
||||
if (alive) {
|
||||
if (mail.navigateOnReuse) {
|
||||
await reuseOrCreateTab(mail.source, mail.url, {
|
||||
inject: mail.inject,
|
||||
injectSource: mail.injectSource,
|
||||
});
|
||||
} else {
|
||||
const tabId = await getTabId(mail.source);
|
||||
await chrome.tabs.update(tabId, { active: true });
|
||||
}
|
||||
} else {
|
||||
await reuseOrCreateTab(mail.source, mail.url, {
|
||||
inject: mail.inject,
|
||||
injectSource: mail.injectSource,
|
||||
});
|
||||
} else {
|
||||
const tabId = await getTabId(mail.source);
|
||||
await chrome.tabs.update(tabId, { active: true });
|
||||
}
|
||||
} else {
|
||||
await reuseOrCreateTab(mail.source, mail.url, {
|
||||
inject: mail.inject,
|
||||
injectSource: mail.injectSource,
|
||||
});
|
||||
}
|
||||
|
||||
await resolveVerificationStep(7, state, mail, {
|
||||
filterAfterTimestamp: stepStartedAt,
|
||||
requestFreshCodeFirst: true,
|
||||
filterAfterTimestamp: mail.provider === HOTMAIL_PROVIDER ? undefined : stepStartedAt,
|
||||
requestFreshCodeFirst: mail.provider === HOTMAIL_PROVIDER ? false : true,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2345,7 +2945,7 @@ async function executeStep9(state) {
|
||||
// Inject scripts directly and wait for them to be ready
|
||||
await chrome.scripting.executeScript({
|
||||
target: { tabId },
|
||||
files: ['content/utils.js', 'content/vps-panel.js'],
|
||||
files: ['content/activation-utils.js', 'content/utils.js', 'content/vps-panel.js'],
|
||||
});
|
||||
await new Promise(r => setTimeout(r, 1000));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user