没有直接撬 Step 1~9 的核心状态机,而是先把最适合独立的管理器和桥接层拆出来
This commit is contained in:
+70
-356
@@ -1,6 +1,9 @@
|
|||||||
// background.js — Service Worker: orchestration, state, tab management, message routing
|
// background.js — Service Worker: orchestration, state, tab management, message routing
|
||||||
|
|
||||||
importScripts(
|
importScripts(
|
||||||
|
'background/panel-bridge.js',
|
||||||
|
'background/generated-email-helpers.js',
|
||||||
|
'background/signup-flow-helpers.js',
|
||||||
'data/names.js',
|
'data/names.js',
|
||||||
'hotmail-utils.js',
|
'hotmail-utils.js',
|
||||||
'microsoft-email.js',
|
'microsoft-email.js',
|
||||||
@@ -5508,186 +5511,52 @@ function getEmailGeneratorLabel(generator) {
|
|||||||
if (generator === CLOUDFLARE_TEMP_EMAIL_GENERATOR) return 'Cloudflare Temp Email';
|
if (generator === CLOUDFLARE_TEMP_EMAIL_GENERATOR) return 'Cloudflare Temp Email';
|
||||||
return 'Duck 邮箱';
|
return 'Duck 邮箱';
|
||||||
}
|
}
|
||||||
|
const generatedEmailHelpers = self.MultiPageGeneratedEmailHelpers?.createGeneratedEmailHelpers({
|
||||||
|
addLog,
|
||||||
|
buildCloudflareTempEmailHeaders,
|
||||||
|
CLOUDFLARE_TEMP_EMAIL_GENERATOR,
|
||||||
|
DUCK_AUTOFILL_URL,
|
||||||
|
fetch,
|
||||||
|
fetchIcloudHideMyEmail,
|
||||||
|
getCloudflareTempEmailAddressFromResponse,
|
||||||
|
getCloudflareTempEmailConfig,
|
||||||
|
getState,
|
||||||
|
joinCloudflareTempEmailUrl,
|
||||||
|
normalizeCloudflareDomain,
|
||||||
|
normalizeCloudflareTempEmailAddress,
|
||||||
|
normalizeEmailGenerator,
|
||||||
|
reuseOrCreateTab,
|
||||||
|
sendToContentScript,
|
||||||
|
setEmailState,
|
||||||
|
throwIfStopped,
|
||||||
|
});
|
||||||
|
|
||||||
function generateCloudflareAliasLocalPart() {
|
function generateCloudflareAliasLocalPart() {
|
||||||
const letters = 'abcdefghijklmnopqrstuvwxyz';
|
return generatedEmailHelpers.generateCloudflareAliasLocalPart();
|
||||||
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 fetchCloudflareEmail(state, options = {}) {
|
async function fetchCloudflareEmail(state, options = {}) {
|
||||||
throwIfStopped();
|
return generatedEmailHelpers.fetchCloudflareEmail(state, options);
|
||||||
const latestState = state || await getState();
|
|
||||||
const domain = normalizeCloudflareDomain(latestState.cloudflareDomain);
|
|
||||||
if (!domain) {
|
|
||||||
throw new Error('Cloudflare 域名为空或格式无效。');
|
|
||||||
}
|
|
||||||
|
|
||||||
const localPart = String(options.localPart || '').trim().toLowerCase() || generateCloudflareAliasLocalPart();
|
|
||||||
const aliasEmail = `${localPart}@${domain}`;
|
|
||||||
|
|
||||||
await setEmailState(aliasEmail);
|
|
||||||
await addLog(`Cloudflare 邮箱:已生成 ${aliasEmail}`, 'ok');
|
|
||||||
return aliasEmail;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureCloudflareTempEmailConfig(state, options = {}) {
|
function ensureCloudflareTempEmailConfig(state, options = {}) {
|
||||||
const {
|
return generatedEmailHelpers.ensureCloudflareTempEmailConfig(state, options);
|
||||||
requireAdminAuth = false,
|
|
||||||
requireDomain = false,
|
|
||||||
} = options;
|
|
||||||
const config = getCloudflareTempEmailConfig(state);
|
|
||||||
if (!config.baseUrl) {
|
|
||||||
throw new Error('Cloudflare Temp Email 服务地址为空或格式无效。');
|
|
||||||
}
|
|
||||||
if (requireAdminAuth && !config.adminAuth) {
|
|
||||||
throw new Error('Cloudflare Temp Email 缺少 Admin Auth。');
|
|
||||||
}
|
|
||||||
if (requireDomain && !config.domain) {
|
|
||||||
throw new Error('Cloudflare Temp Email 域名为空或格式无效。');
|
|
||||||
}
|
|
||||||
return config;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function requestCloudflareTempEmailJson(config, path, options = {}) {
|
async function requestCloudflareTempEmailJson(config, path, options = {}) {
|
||||||
const {
|
return generatedEmailHelpers.requestCloudflareTempEmailJson(config, path, options);
|
||||||
method = 'GET',
|
|
||||||
payload,
|
|
||||||
searchParams,
|
|
||||||
timeoutMs = 20000,
|
|
||||||
} = options;
|
|
||||||
|
|
||||||
const url = new URL(joinCloudflareTempEmailUrl(config.baseUrl, path));
|
|
||||||
if (searchParams && typeof searchParams === 'object') {
|
|
||||||
for (const [key, value] of Object.entries(searchParams)) {
|
|
||||||
if (value === undefined || value === null || value === '') continue;
|
|
||||||
url.searchParams.set(key, String(value));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const controller = new AbortController();
|
|
||||||
const timeoutId = setTimeout(() => controller.abort(new Error('timeout')), timeoutMs);
|
|
||||||
|
|
||||||
let response;
|
|
||||||
try {
|
|
||||||
response = await fetch(url.toString(), {
|
|
||||||
method,
|
|
||||||
headers: buildCloudflareTempEmailHeaders(config, {
|
|
||||||
json: payload !== undefined,
|
|
||||||
}),
|
|
||||||
body: payload !== undefined ? JSON.stringify(payload) : undefined,
|
|
||||||
signal: controller.signal,
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
const errorMessage = err?.name === 'AbortError'
|
|
||||||
? `Cloudflare Temp Email 请求超时(>${Math.round(timeoutMs / 1000)} 秒)`
|
|
||||||
: `Cloudflare Temp Email 请求失败:${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(`Cloudflare Temp Email 请求失败:${payloadError || text || `HTTP ${response.status}`}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return parsed;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchCloudflareTempEmailAddress(state, options = {}) {
|
async function fetchCloudflareTempEmailAddress(state, options = {}) {
|
||||||
throwIfStopped();
|
return generatedEmailHelpers.fetchCloudflareTempEmailAddress(state, options);
|
||||||
const latestState = state || await getState();
|
|
||||||
const config = ensureCloudflareTempEmailConfig(latestState, {
|
|
||||||
requireAdminAuth: true,
|
|
||||||
requireDomain: true,
|
|
||||||
});
|
|
||||||
const requestedName = String(options.localPart || options.name || '').trim().toLowerCase() || generateCloudflareAliasLocalPart();
|
|
||||||
const payload = {
|
|
||||||
enablePrefix: true,
|
|
||||||
name: requestedName,
|
|
||||||
domain: config.domain,
|
|
||||||
};
|
|
||||||
const result = await requestCloudflareTempEmailJson(config, '/admin/new_address', {
|
|
||||||
method: 'POST',
|
|
||||||
payload,
|
|
||||||
});
|
|
||||||
const address = normalizeCloudflareTempEmailAddress(getCloudflareTempEmailAddressFromResponse(result));
|
|
||||||
if (!address) {
|
|
||||||
throw new Error('Cloudflare Temp Email 未返回可用邮箱地址。');
|
|
||||||
}
|
|
||||||
|
|
||||||
await setEmailState(address);
|
|
||||||
await addLog(`Cloudflare Temp Email:已生成 ${address}`, 'ok');
|
|
||||||
return address;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchDuckEmail(options = {}) {
|
async function fetchDuckEmail(options = {}) {
|
||||||
throwIfStopped();
|
return generatedEmailHelpers.fetchDuckEmail(options);
|
||||||
const { generateNew = true } = options;
|
|
||||||
|
|
||||||
await addLog(`Duck 邮箱:正在打开自动填充设置(${generateNew ? '生成新地址' : '复用当前地址'})...`);
|
|
||||||
await reuseOrCreateTab('duck-mail', DUCK_AUTOFILL_URL);
|
|
||||||
|
|
||||||
const result = await sendToContentScript('duck-mail', {
|
|
||||||
type: 'FETCH_DUCK_EMAIL',
|
|
||||||
source: 'background',
|
|
||||||
payload: { generateNew },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result?.error) {
|
|
||||||
throw new Error(result.error);
|
|
||||||
}
|
|
||||||
if (!result?.email) {
|
|
||||||
throw new Error('未返回 Duck 邮箱地址。');
|
|
||||||
}
|
|
||||||
|
|
||||||
await setEmailState(result.email);
|
|
||||||
await addLog(`Duck 邮箱:${result.generated ? '已生成' : '已读取'} ${result.email}`, 'ok');
|
|
||||||
return result.email;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchGeneratedEmail(state, options = {}) {
|
async function fetchGeneratedEmail(state, options = {}) {
|
||||||
const currentState = state || await getState();
|
return generatedEmailHelpers.fetchGeneratedEmail(state, options);
|
||||||
const generator = normalizeEmailGenerator(options.generator ?? currentState.emailGenerator);
|
|
||||||
if (generator === 'custom') {
|
|
||||||
throw new Error('当前邮箱生成方式为自定义邮箱,请直接填写注册邮箱。');
|
|
||||||
}
|
|
||||||
if (generator === 'icloud') {
|
|
||||||
return fetchIcloudHideMyEmail();
|
|
||||||
}
|
|
||||||
if (generator === 'cloudflare') {
|
|
||||||
return fetchCloudflareEmail(currentState, options);
|
|
||||||
}
|
|
||||||
if (generator === CLOUDFLARE_TEMP_EMAIL_GENERATOR) {
|
|
||||||
return fetchCloudflareTempEmailAddress(currentState, options);
|
|
||||||
}
|
|
||||||
return fetchDuckEmail(options);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
@@ -6504,219 +6373,67 @@ async function resumeAutoRun() {
|
|||||||
|
|
||||||
const SIGNUP_ENTRY_URL = 'https://chatgpt.com/';
|
const SIGNUP_ENTRY_URL = 'https://chatgpt.com/';
|
||||||
const SIGNUP_PAGE_INJECT_FILES = ['content/utils.js', 'content/signup-page.js'];
|
const SIGNUP_PAGE_INJECT_FILES = ['content/utils.js', 'content/signup-page.js'];
|
||||||
|
const panelBridge = self.MultiPageBackgroundPanelBridge?.createPanelBridge({
|
||||||
|
chrome,
|
||||||
|
addLog,
|
||||||
|
closeConflictingTabsForSource,
|
||||||
|
ensureContentScriptReadyOnTab,
|
||||||
|
getPanelMode,
|
||||||
|
normalizeSub2ApiUrl,
|
||||||
|
rememberSourceLastUrl,
|
||||||
|
sendToContentScript,
|
||||||
|
sendToContentScriptResilient,
|
||||||
|
waitForTabUrlFamily,
|
||||||
|
DEFAULT_SUB2API_GROUP_NAME,
|
||||||
|
SUB2API_STEP1_RESPONSE_TIMEOUT_MS,
|
||||||
|
});
|
||||||
|
const signupFlowHelpers = self.MultiPageSignupFlowHelpers?.createSignupFlowHelpers({
|
||||||
|
addLog,
|
||||||
|
buildGeneratedAliasEmail,
|
||||||
|
chrome,
|
||||||
|
ensureContentScriptReadyOnTab,
|
||||||
|
ensureHotmailAccountForFlow,
|
||||||
|
ensureLuckmailPurchaseForFlow,
|
||||||
|
getTabId,
|
||||||
|
isGeneratedAliasProvider,
|
||||||
|
isHotmailProvider,
|
||||||
|
isLuckmailProvider,
|
||||||
|
isSignupPasswordPageUrl,
|
||||||
|
isTabAlive,
|
||||||
|
reuseOrCreateTab,
|
||||||
|
sendToContentScriptResilient,
|
||||||
|
setEmailState,
|
||||||
|
SIGNUP_ENTRY_URL,
|
||||||
|
SIGNUP_PAGE_INJECT_FILES,
|
||||||
|
waitForTabUrlMatch,
|
||||||
|
});
|
||||||
|
|
||||||
async function requestOAuthUrlFromPanel(state, options = {}) {
|
async function requestOAuthUrlFromPanel(state, options = {}) {
|
||||||
if (getPanelMode(state) === 'sub2api') {
|
return panelBridge.requestOAuthUrlFromPanel(state, options);
|
||||||
return requestSub2ApiOAuthUrl(state, options);
|
|
||||||
}
|
|
||||||
return requestCpaOAuthUrl(state, options);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function requestCpaOAuthUrl(state, options = {}) {
|
async function requestCpaOAuthUrl(state, options = {}) {
|
||||||
const { logLabel = 'OAuth 刷新' } = options;
|
return panelBridge.requestCpaOAuthUrl(state, options);
|
||||||
if (!state.vpsUrl) {
|
|
||||||
throw new Error('尚未配置 CPA 地址,请先在侧边栏填写。');
|
|
||||||
}
|
|
||||||
|
|
||||||
await addLog(`${logLabel}:正在打开 CPA 面板...`);
|
|
||||||
|
|
||||||
const injectFiles = ['content/activation-utils.js', 'content/utils.js', 'content/vps-panel.js'];
|
|
||||||
await closeConflictingTabsForSource('vps-panel', state.vpsUrl);
|
|
||||||
|
|
||||||
const tab = await chrome.tabs.create({ url: state.vpsUrl, active: true });
|
|
||||||
const tabId = tab.id;
|
|
||||||
await rememberSourceLastUrl('vps-panel', state.vpsUrl);
|
|
||||||
|
|
||||||
await addLog(`${logLabel}:CPA 面板已打开,正在等待页面进入目标地址...`);
|
|
||||||
const matchedTab = await waitForTabUrlFamily('vps-panel', tabId, state.vpsUrl, {
|
|
||||||
timeoutMs: 15000,
|
|
||||||
retryDelayMs: 400,
|
|
||||||
});
|
|
||||||
if (!matchedTab) {
|
|
||||||
await addLog(`${logLabel}:CPA 页面尚未完全进入目标地址,继续尝试连接内容脚本...`, 'warn');
|
|
||||||
}
|
|
||||||
|
|
||||||
await ensureContentScriptReadyOnTab('vps-panel', tabId, {
|
|
||||||
inject: injectFiles,
|
|
||||||
timeoutMs: 45000,
|
|
||||||
retryDelayMs: 900,
|
|
||||||
logMessage: `${logLabel}:CPA 面板仍在加载,正在重试连接内容脚本...`,
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await sendToContentScriptResilient('vps-panel', {
|
|
||||||
type: 'REQUEST_OAUTH_URL',
|
|
||||||
source: 'background',
|
|
||||||
payload: {
|
|
||||||
vpsPassword: state.vpsPassword,
|
|
||||||
logStep: 6,
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
timeoutMs: 30000,
|
|
||||||
retryDelayMs: 700,
|
|
||||||
logMessage: `${logLabel}:CPA 面板通信未就绪,正在等待页面恢复...`,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result?.error) {
|
|
||||||
throw new Error(result.error);
|
|
||||||
}
|
|
||||||
return result || {};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function requestSub2ApiOAuthUrl(state, options = {}) {
|
async function requestSub2ApiOAuthUrl(state, options = {}) {
|
||||||
const { logLabel = 'OAuth 刷新' } = options;
|
return panelBridge.requestSub2ApiOAuthUrl(state, options);
|
||||||
const sub2apiUrl = normalizeSub2ApiUrl(state.sub2apiUrl);
|
|
||||||
const groupName = (state.sub2apiGroupName || DEFAULT_SUB2API_GROUP_NAME).trim() || DEFAULT_SUB2API_GROUP_NAME;
|
|
||||||
|
|
||||||
if (!state.sub2apiEmail) {
|
|
||||||
throw new Error('尚未配置 SUB2API 登录邮箱,请先在侧边栏填写。');
|
|
||||||
}
|
|
||||||
if (!state.sub2apiPassword) {
|
|
||||||
throw new Error('尚未配置 SUB2API 登录密码,请先在侧边栏填写。');
|
|
||||||
}
|
|
||||||
|
|
||||||
await addLog(`${logLabel}:正在打开 SUB2API 后台...`);
|
|
||||||
|
|
||||||
const injectFiles = ['content/utils.js', 'content/sub2api-panel.js'];
|
|
||||||
await closeConflictingTabsForSource('sub2api-panel', sub2apiUrl);
|
|
||||||
|
|
||||||
const tab = await chrome.tabs.create({ url: sub2apiUrl, active: true });
|
|
||||||
const tabId = tab.id;
|
|
||||||
await rememberSourceLastUrl('sub2api-panel', sub2apiUrl);
|
|
||||||
|
|
||||||
await addLog(`${logLabel}:SUB2API 页面已打开,正在等待页面进入目标地址...`);
|
|
||||||
const matchedTab = await waitForTabUrlFamily('sub2api-panel', tabId, sub2apiUrl, {
|
|
||||||
timeoutMs: 15000,
|
|
||||||
retryDelayMs: 400,
|
|
||||||
});
|
|
||||||
if (!matchedTab) {
|
|
||||||
await addLog(`${logLabel}:SUB2API 页面尚未稳定,继续尝试连接内容脚本...`, 'warn');
|
|
||||||
}
|
|
||||||
|
|
||||||
await ensureContentScriptReadyOnTab('sub2api-panel', tabId, {
|
|
||||||
inject: injectFiles,
|
|
||||||
injectSource: 'sub2api-panel',
|
|
||||||
timeoutMs: 45000,
|
|
||||||
retryDelayMs: 900,
|
|
||||||
logMessage: `${logLabel}:SUB2API 页面仍在加载,正在重试连接内容脚本...`,
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await sendToContentScript('sub2api-panel', {
|
|
||||||
type: 'REQUEST_OAUTH_URL',
|
|
||||||
source: 'background',
|
|
||||||
payload: {
|
|
||||||
sub2apiUrl,
|
|
||||||
sub2apiEmail: state.sub2apiEmail,
|
|
||||||
sub2apiPassword: state.sub2apiPassword,
|
|
||||||
sub2apiGroupName: groupName,
|
|
||||||
logStep: 6,
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
responseTimeoutMs: SUB2API_STEP1_RESPONSE_TIMEOUT_MS,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result?.error) {
|
|
||||||
throw new Error(result.error);
|
|
||||||
}
|
|
||||||
return result || {};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function openSignupEntryTab(step = 1) {
|
async function openSignupEntryTab(step = 1) {
|
||||||
const tabId = await reuseOrCreateTab('signup-page', SIGNUP_ENTRY_URL, {
|
return signupFlowHelpers.openSignupEntryTab(step);
|
||||||
inject: SIGNUP_PAGE_INJECT_FILES,
|
|
||||||
injectSource: 'signup-page',
|
|
||||||
});
|
|
||||||
|
|
||||||
await ensureContentScriptReadyOnTab('signup-page', tabId, {
|
|
||||||
inject: SIGNUP_PAGE_INJECT_FILES,
|
|
||||||
injectSource: 'signup-page',
|
|
||||||
timeoutMs: 45000,
|
|
||||||
retryDelayMs: 900,
|
|
||||||
logMessage: `步骤 ${step}:ChatGPT 官网仍在加载,正在重试连接内容脚本...`,
|
|
||||||
});
|
|
||||||
|
|
||||||
return tabId;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function ensureSignupEntryPageReady(step = 1) {
|
async function ensureSignupEntryPageReady(step = 1) {
|
||||||
const tabId = await openSignupEntryTab(step);
|
return signupFlowHelpers.ensureSignupEntryPageReady(step);
|
||||||
const result = await sendToContentScriptResilient('signup-page', {
|
|
||||||
type: 'ENSURE_SIGNUP_ENTRY_READY',
|
|
||||||
step,
|
|
||||||
source: 'background',
|
|
||||||
payload: {},
|
|
||||||
}, {
|
|
||||||
timeoutMs: 20000,
|
|
||||||
retryDelayMs: 700,
|
|
||||||
logMessage: `步骤 ${step}:官网注册入口正在切换,等待页面恢复...`,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result?.error) {
|
|
||||||
throw new Error(result.error);
|
|
||||||
}
|
|
||||||
|
|
||||||
return { tabId, result: result || {} };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function ensureSignupPasswordPageReadyInTab(tabId, step = 2, options = {}) {
|
async function ensureSignupPasswordPageReadyInTab(tabId, step = 2, options = {}) {
|
||||||
const { skipUrlWait = false } = options;
|
return signupFlowHelpers.ensureSignupPasswordPageReadyInTab(tabId, step, options);
|
||||||
|
|
||||||
if (!skipUrlWait) {
|
|
||||||
const matchedTab = await waitForTabUrlMatch(tabId, (url) => isSignupPasswordPageUrl(url), {
|
|
||||||
timeoutMs: 45000,
|
|
||||||
retryDelayMs: 300,
|
|
||||||
});
|
|
||||||
if (!matchedTab) {
|
|
||||||
throw new Error('等待进入密码页超时,请检查邮箱提交后页面是否仍停留在官网或邮箱页。');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await ensureContentScriptReadyOnTab('signup-page', tabId, {
|
|
||||||
inject: SIGNUP_PAGE_INJECT_FILES,
|
|
||||||
injectSource: 'signup-page',
|
|
||||||
timeoutMs: 45000,
|
|
||||||
retryDelayMs: 900,
|
|
||||||
logMessage: `步骤 ${step}:密码页仍在加载,正在重试连接内容脚本...`,
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await sendToContentScriptResilient('signup-page', {
|
|
||||||
type: 'ENSURE_SIGNUP_PASSWORD_PAGE_READY',
|
|
||||||
step,
|
|
||||||
source: 'background',
|
|
||||||
payload: {},
|
|
||||||
}, {
|
|
||||||
timeoutMs: 20000,
|
|
||||||
retryDelayMs: 700,
|
|
||||||
logMessage: `步骤 ${step}:认证页正在切换,等待密码页重新就绪...`,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result?.error) {
|
|
||||||
throw new Error(result.error);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result || {};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function resolveSignupEmailForFlow(state) {
|
async function resolveSignupEmailForFlow(state) {
|
||||||
let resolvedEmail = state.email;
|
return signupFlowHelpers.resolveSignupEmailForFlow(state);
|
||||||
if (isHotmailProvider(state)) {
|
|
||||||
const account = await ensureHotmailAccountForFlow({
|
|
||||||
allowAllocate: true,
|
|
||||||
markUsed: true,
|
|
||||||
preferredAccountId: state.currentHotmailAccountId || null,
|
|
||||||
});
|
|
||||||
resolvedEmail = account.email;
|
|
||||||
} else if (isLuckmailProvider(state)) {
|
|
||||||
const purchase = await ensureLuckmailPurchaseForFlow({ allowReuse: true });
|
|
||||||
resolvedEmail = purchase.email_address;
|
|
||||||
} else if (isGeneratedAliasProvider(state)) {
|
|
||||||
resolvedEmail = buildGeneratedAliasEmail(state);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!resolvedEmail) {
|
|
||||||
throw new Error('缺少邮箱地址,请先在侧边栏粘贴邮箱。');
|
|
||||||
}
|
|
||||||
|
|
||||||
return resolvedEmail;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
@@ -6735,9 +6452,6 @@ async function executeStep1() {
|
|||||||
|
|
||||||
async function executeStep2(state) {
|
async function executeStep2(state) {
|
||||||
const resolvedEmail = await resolveSignupEmailForFlow(state);
|
const resolvedEmail = await resolveSignupEmailForFlow(state);
|
||||||
if (resolvedEmail !== state.email) {
|
|
||||||
await setEmailState(resolvedEmail);
|
|
||||||
}
|
|
||||||
|
|
||||||
let signupTabId = await getTabId('signup-page');
|
let signupTabId = await getTabId('signup-page');
|
||||||
if (!signupTabId || !(await isTabAlive('signup-page'))) {
|
if (!signupTabId || !(await isTabAlive('signup-page'))) {
|
||||||
|
|||||||
@@ -0,0 +1,220 @@
|
|||||||
|
(function attachGeneratedEmailHelpers(root, factory) {
|
||||||
|
root.MultiPageGeneratedEmailHelpers = factory();
|
||||||
|
})(typeof self !== 'undefined' ? self : globalThis, function createGeneratedEmailHelpersModule() {
|
||||||
|
function createGeneratedEmailHelpers(deps = {}) {
|
||||||
|
const {
|
||||||
|
addLog,
|
||||||
|
buildCloudflareTempEmailHeaders,
|
||||||
|
CLOUDFLARE_TEMP_EMAIL_GENERATOR,
|
||||||
|
DUCK_AUTOFILL_URL,
|
||||||
|
fetch,
|
||||||
|
fetchIcloudHideMyEmail,
|
||||||
|
getCloudflareTempEmailAddressFromResponse,
|
||||||
|
getCloudflareTempEmailConfig,
|
||||||
|
getState,
|
||||||
|
joinCloudflareTempEmailUrl,
|
||||||
|
normalizeCloudflareDomain,
|
||||||
|
normalizeCloudflareTempEmailAddress,
|
||||||
|
normalizeEmailGenerator,
|
||||||
|
reuseOrCreateTab,
|
||||||
|
sendToContentScript,
|
||||||
|
setEmailState,
|
||||||
|
throwIfStopped,
|
||||||
|
} = deps;
|
||||||
|
|
||||||
|
function generateCloudflareAliasLocalPart() {
|
||||||
|
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 fetchCloudflareEmail(state, options = {}) {
|
||||||
|
throwIfStopped();
|
||||||
|
const latestState = state || await getState();
|
||||||
|
const domain = normalizeCloudflareDomain(latestState.cloudflareDomain);
|
||||||
|
if (!domain) {
|
||||||
|
throw new Error('Cloudflare 域名为空或格式无效。');
|
||||||
|
}
|
||||||
|
|
||||||
|
const localPart = String(options.localPart || '').trim().toLowerCase() || generateCloudflareAliasLocalPart();
|
||||||
|
const aliasEmail = `${localPart}@${domain}`;
|
||||||
|
|
||||||
|
await setEmailState(aliasEmail);
|
||||||
|
await addLog(`Cloudflare 邮箱:已生成 ${aliasEmail}`, 'ok');
|
||||||
|
return aliasEmail;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureCloudflareTempEmailConfig(state, options = {}) {
|
||||||
|
const {
|
||||||
|
requireAdminAuth = false,
|
||||||
|
requireDomain = false,
|
||||||
|
} = options;
|
||||||
|
const config = getCloudflareTempEmailConfig(state);
|
||||||
|
if (!config.baseUrl) {
|
||||||
|
throw new Error('Cloudflare Temp Email 服务地址为空或格式无效。');
|
||||||
|
}
|
||||||
|
if (requireAdminAuth && !config.adminAuth) {
|
||||||
|
throw new Error('Cloudflare Temp Email 缺少 Admin Auth。');
|
||||||
|
}
|
||||||
|
if (requireDomain && !config.domain) {
|
||||||
|
throw new Error('Cloudflare Temp Email 域名为空或格式无效。');
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function requestCloudflareTempEmailJson(config, path, options = {}) {
|
||||||
|
const {
|
||||||
|
method = 'GET',
|
||||||
|
payload,
|
||||||
|
searchParams,
|
||||||
|
timeoutMs = 20000,
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
const url = new URL(joinCloudflareTempEmailUrl(config.baseUrl, path));
|
||||||
|
if (searchParams && typeof searchParams === 'object') {
|
||||||
|
for (const [key, value] of Object.entries(searchParams)) {
|
||||||
|
if (value === undefined || value === null || value === '') continue;
|
||||||
|
url.searchParams.set(key, String(value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timeoutId = setTimeout(() => controller.abort(new Error('timeout')), timeoutMs);
|
||||||
|
|
||||||
|
let response;
|
||||||
|
try {
|
||||||
|
response = await fetch(url.toString(), {
|
||||||
|
method,
|
||||||
|
headers: buildCloudflareTempEmailHeaders(config, {
|
||||||
|
json: payload !== undefined,
|
||||||
|
}),
|
||||||
|
body: payload !== undefined ? JSON.stringify(payload) : undefined,
|
||||||
|
signal: controller.signal,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
const errorMessage = err?.name === 'AbortError'
|
||||||
|
? `Cloudflare Temp Email 请求超时(>${Math.round(timeoutMs / 1000)} 秒)`
|
||||||
|
: `Cloudflare Temp Email 请求失败:${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(`Cloudflare Temp Email 请求失败:${payloadError || text || `HTTP ${response.status}`}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchCloudflareTempEmailAddress(state, options = {}) {
|
||||||
|
throwIfStopped();
|
||||||
|
const latestState = state || await getState();
|
||||||
|
const config = ensureCloudflareTempEmailConfig(latestState, {
|
||||||
|
requireAdminAuth: true,
|
||||||
|
requireDomain: true,
|
||||||
|
});
|
||||||
|
const requestedName = String(options.localPart || options.name || '').trim().toLowerCase() || generateCloudflareAliasLocalPart();
|
||||||
|
const payload = {
|
||||||
|
enablePrefix: true,
|
||||||
|
name: requestedName,
|
||||||
|
domain: config.domain,
|
||||||
|
};
|
||||||
|
const result = await requestCloudflareTempEmailJson(config, '/admin/new_address', {
|
||||||
|
method: 'POST',
|
||||||
|
payload,
|
||||||
|
});
|
||||||
|
const address = normalizeCloudflareTempEmailAddress(getCloudflareTempEmailAddressFromResponse(result));
|
||||||
|
if (!address) {
|
||||||
|
throw new Error('Cloudflare Temp Email 未返回可用邮箱地址。');
|
||||||
|
}
|
||||||
|
|
||||||
|
await setEmailState(address);
|
||||||
|
await addLog(`Cloudflare Temp Email:已生成 ${address}`, 'ok');
|
||||||
|
return address;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchDuckEmail(options = {}) {
|
||||||
|
throwIfStopped();
|
||||||
|
const { generateNew = true } = options;
|
||||||
|
|
||||||
|
await addLog(`Duck 邮箱:正在打开自动填充设置(${generateNew ? '生成新地址' : '复用当前地址'})...`);
|
||||||
|
await reuseOrCreateTab('duck-mail', DUCK_AUTOFILL_URL);
|
||||||
|
|
||||||
|
const result = await sendToContentScript('duck-mail', {
|
||||||
|
type: 'FETCH_DUCK_EMAIL',
|
||||||
|
source: 'background',
|
||||||
|
payload: { generateNew },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result?.error) {
|
||||||
|
throw new Error(result.error);
|
||||||
|
}
|
||||||
|
if (!result?.email) {
|
||||||
|
throw new Error('未返回 Duck 邮箱地址。');
|
||||||
|
}
|
||||||
|
|
||||||
|
await setEmailState(result.email);
|
||||||
|
await addLog(`Duck 邮箱:${result.generated ? '已生成' : '已读取'} ${result.email}`, 'ok');
|
||||||
|
return result.email;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchGeneratedEmail(state, options = {}) {
|
||||||
|
const currentState = state || await getState();
|
||||||
|
const generator = normalizeEmailGenerator(options.generator ?? currentState.emailGenerator);
|
||||||
|
if (generator === 'custom') {
|
||||||
|
throw new Error('当前邮箱生成方式为自定义邮箱,请直接填写注册邮箱。');
|
||||||
|
}
|
||||||
|
if (generator === 'icloud') {
|
||||||
|
return fetchIcloudHideMyEmail();
|
||||||
|
}
|
||||||
|
if (generator === 'cloudflare') {
|
||||||
|
return fetchCloudflareEmail(currentState, options);
|
||||||
|
}
|
||||||
|
if (generator === CLOUDFLARE_TEMP_EMAIL_GENERATOR) {
|
||||||
|
return fetchCloudflareTempEmailAddress(currentState, options);
|
||||||
|
}
|
||||||
|
return fetchDuckEmail(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
ensureCloudflareTempEmailConfig,
|
||||||
|
fetchCloudflareEmail,
|
||||||
|
fetchCloudflareTempEmailAddress,
|
||||||
|
fetchDuckEmail,
|
||||||
|
fetchGeneratedEmail,
|
||||||
|
generateCloudflareAliasLocalPart,
|
||||||
|
requestCloudflareTempEmailJson,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
createGeneratedEmailHelpers,
|
||||||
|
};
|
||||||
|
});
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
(function attachBackgroundPanelBridge(root, factory) {
|
||||||
|
root.MultiPageBackgroundPanelBridge = factory();
|
||||||
|
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundPanelBridgeModule() {
|
||||||
|
function createPanelBridge(deps = {}) {
|
||||||
|
const {
|
||||||
|
chrome,
|
||||||
|
addLog,
|
||||||
|
closeConflictingTabsForSource,
|
||||||
|
ensureContentScriptReadyOnTab,
|
||||||
|
getPanelMode,
|
||||||
|
normalizeSub2ApiUrl,
|
||||||
|
rememberSourceLastUrl,
|
||||||
|
sendToContentScript,
|
||||||
|
sendToContentScriptResilient,
|
||||||
|
waitForTabUrlFamily,
|
||||||
|
DEFAULT_SUB2API_GROUP_NAME,
|
||||||
|
SUB2API_STEP1_RESPONSE_TIMEOUT_MS,
|
||||||
|
} = deps;
|
||||||
|
|
||||||
|
async function requestOAuthUrlFromPanel(state, options = {}) {
|
||||||
|
if (getPanelMode(state) === 'sub2api') {
|
||||||
|
return requestSub2ApiOAuthUrl(state, options);
|
||||||
|
}
|
||||||
|
return requestCpaOAuthUrl(state, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function requestCpaOAuthUrl(state, options = {}) {
|
||||||
|
const { logLabel = 'OAuth 刷新' } = options;
|
||||||
|
if (!state.vpsUrl) {
|
||||||
|
throw new Error('尚未配置 CPA 地址,请先在侧边栏填写。');
|
||||||
|
}
|
||||||
|
|
||||||
|
await addLog(`${logLabel}:正在打开 CPA 面板...`);
|
||||||
|
|
||||||
|
const injectFiles = ['content/activation-utils.js', 'content/utils.js', 'content/vps-panel.js'];
|
||||||
|
await closeConflictingTabsForSource('vps-panel', state.vpsUrl);
|
||||||
|
|
||||||
|
const tab = await chrome.tabs.create({ url: state.vpsUrl, active: true });
|
||||||
|
const tabId = tab.id;
|
||||||
|
await rememberSourceLastUrl('vps-panel', state.vpsUrl);
|
||||||
|
|
||||||
|
await addLog(`${logLabel}:CPA 面板已打开,正在等待页面进入目标地址...`);
|
||||||
|
const matchedTab = await waitForTabUrlFamily('vps-panel', tabId, state.vpsUrl, {
|
||||||
|
timeoutMs: 15000,
|
||||||
|
retryDelayMs: 400,
|
||||||
|
});
|
||||||
|
if (!matchedTab) {
|
||||||
|
await addLog(`${logLabel}:CPA 页面尚未完全进入目标地址,继续尝试连接内容脚本...`, 'warn');
|
||||||
|
}
|
||||||
|
|
||||||
|
await ensureContentScriptReadyOnTab('vps-panel', tabId, {
|
||||||
|
inject: injectFiles,
|
||||||
|
timeoutMs: 45000,
|
||||||
|
retryDelayMs: 900,
|
||||||
|
logMessage: `${logLabel}:CPA 面板仍在加载,正在重试连接内容脚本...`,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await sendToContentScriptResilient('vps-panel', {
|
||||||
|
type: 'REQUEST_OAUTH_URL',
|
||||||
|
source: 'background',
|
||||||
|
payload: {
|
||||||
|
vpsPassword: state.vpsPassword,
|
||||||
|
logStep: 6,
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
timeoutMs: 30000,
|
||||||
|
retryDelayMs: 700,
|
||||||
|
logMessage: `${logLabel}:CPA 面板通信未就绪,正在等待页面恢复...`,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result?.error) {
|
||||||
|
throw new Error(result.error);
|
||||||
|
}
|
||||||
|
return result || {};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function requestSub2ApiOAuthUrl(state, options = {}) {
|
||||||
|
const { logLabel = 'OAuth 刷新' } = options;
|
||||||
|
const sub2apiUrl = normalizeSub2ApiUrl(state.sub2apiUrl);
|
||||||
|
const groupName = (state.sub2apiGroupName || DEFAULT_SUB2API_GROUP_NAME).trim() || DEFAULT_SUB2API_GROUP_NAME;
|
||||||
|
|
||||||
|
if (!state.sub2apiEmail) {
|
||||||
|
throw new Error('尚未配置 SUB2API 登录邮箱,请先在侧边栏填写。');
|
||||||
|
}
|
||||||
|
if (!state.sub2apiPassword) {
|
||||||
|
throw new Error('尚未配置 SUB2API 登录密码,请先在侧边栏填写。');
|
||||||
|
}
|
||||||
|
|
||||||
|
await addLog(`${logLabel}:正在打开 SUB2API 后台...`);
|
||||||
|
|
||||||
|
const injectFiles = ['content/utils.js', 'content/sub2api-panel.js'];
|
||||||
|
await closeConflictingTabsForSource('sub2api-panel', sub2apiUrl);
|
||||||
|
|
||||||
|
const tab = await chrome.tabs.create({ url: sub2apiUrl, active: true });
|
||||||
|
const tabId = tab.id;
|
||||||
|
await rememberSourceLastUrl('sub2api-panel', sub2apiUrl);
|
||||||
|
|
||||||
|
await addLog(`${logLabel}:SUB2API 页面已打开,正在等待页面进入目标地址...`);
|
||||||
|
const matchedTab = await waitForTabUrlFamily('sub2api-panel', tabId, sub2apiUrl, {
|
||||||
|
timeoutMs: 15000,
|
||||||
|
retryDelayMs: 400,
|
||||||
|
});
|
||||||
|
if (!matchedTab) {
|
||||||
|
await addLog(`${logLabel}:SUB2API 页面尚未稳定,继续尝试连接内容脚本...`, 'warn');
|
||||||
|
}
|
||||||
|
|
||||||
|
await ensureContentScriptReadyOnTab('sub2api-panel', tabId, {
|
||||||
|
inject: injectFiles,
|
||||||
|
injectSource: 'sub2api-panel',
|
||||||
|
timeoutMs: 45000,
|
||||||
|
retryDelayMs: 900,
|
||||||
|
logMessage: `${logLabel}:SUB2API 页面仍在加载,正在重试连接内容脚本...`,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await sendToContentScript('sub2api-panel', {
|
||||||
|
type: 'REQUEST_OAUTH_URL',
|
||||||
|
source: 'background',
|
||||||
|
payload: {
|
||||||
|
sub2apiUrl,
|
||||||
|
sub2apiEmail: state.sub2apiEmail,
|
||||||
|
sub2apiPassword: state.sub2apiPassword,
|
||||||
|
sub2apiGroupName: groupName,
|
||||||
|
logStep: 6,
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
responseTimeoutMs: SUB2API_STEP1_RESPONSE_TIMEOUT_MS,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result?.error) {
|
||||||
|
throw new Error(result.error);
|
||||||
|
}
|
||||||
|
return result || {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
requestOAuthUrlFromPanel,
|
||||||
|
requestCpaOAuthUrl,
|
||||||
|
requestSub2ApiOAuthUrl,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
createPanelBridge,
|
||||||
|
};
|
||||||
|
});
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
(function attachSignupFlowHelpers(root, factory) {
|
||||||
|
root.MultiPageSignupFlowHelpers = factory();
|
||||||
|
})(typeof self !== 'undefined' ? self : globalThis, function createSignupFlowHelpersModule() {
|
||||||
|
function createSignupFlowHelpers(deps = {}) {
|
||||||
|
const {
|
||||||
|
addLog,
|
||||||
|
buildGeneratedAliasEmail,
|
||||||
|
chrome,
|
||||||
|
ensureContentScriptReadyOnTab,
|
||||||
|
ensureHotmailAccountForFlow,
|
||||||
|
ensureLuckmailPurchaseForFlow,
|
||||||
|
getTabId,
|
||||||
|
isGeneratedAliasProvider,
|
||||||
|
isHotmailProvider,
|
||||||
|
isLuckmailProvider,
|
||||||
|
isSignupPasswordPageUrl,
|
||||||
|
isTabAlive,
|
||||||
|
reuseOrCreateTab,
|
||||||
|
sendToContentScriptResilient,
|
||||||
|
setEmailState,
|
||||||
|
SIGNUP_ENTRY_URL,
|
||||||
|
SIGNUP_PAGE_INJECT_FILES,
|
||||||
|
waitForTabUrlMatch,
|
||||||
|
} = deps;
|
||||||
|
|
||||||
|
async function openSignupEntryTab(step = 1) {
|
||||||
|
const tabId = await reuseOrCreateTab('signup-page', SIGNUP_ENTRY_URL, {
|
||||||
|
inject: SIGNUP_PAGE_INJECT_FILES,
|
||||||
|
injectSource: 'signup-page',
|
||||||
|
});
|
||||||
|
|
||||||
|
await ensureContentScriptReadyOnTab('signup-page', tabId, {
|
||||||
|
inject: SIGNUP_PAGE_INJECT_FILES,
|
||||||
|
injectSource: 'signup-page',
|
||||||
|
timeoutMs: 45000,
|
||||||
|
retryDelayMs: 900,
|
||||||
|
logMessage: `步骤 ${step}:ChatGPT 官网仍在加载,正在重试连接内容脚本...`,
|
||||||
|
});
|
||||||
|
|
||||||
|
return tabId;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ensureSignupEntryPageReady(step = 1) {
|
||||||
|
const tabId = await openSignupEntryTab(step);
|
||||||
|
const result = await sendToContentScriptResilient('signup-page', {
|
||||||
|
type: 'ENSURE_SIGNUP_ENTRY_READY',
|
||||||
|
step,
|
||||||
|
source: 'background',
|
||||||
|
payload: {},
|
||||||
|
}, {
|
||||||
|
timeoutMs: 20000,
|
||||||
|
retryDelayMs: 700,
|
||||||
|
logMessage: `步骤 ${step}:官网注册入口正在切换,等待页面恢复...`,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result?.error) {
|
||||||
|
throw new Error(result.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { tabId, result: result || {} };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ensureSignupPasswordPageReadyInTab(tabId, step = 2, options = {}) {
|
||||||
|
const { skipUrlWait = false } = options;
|
||||||
|
|
||||||
|
if (!skipUrlWait) {
|
||||||
|
const matchedTab = await waitForTabUrlMatch(tabId, (url) => isSignupPasswordPageUrl(url), {
|
||||||
|
timeoutMs: 45000,
|
||||||
|
retryDelayMs: 300,
|
||||||
|
});
|
||||||
|
if (!matchedTab) {
|
||||||
|
throw new Error('等待进入密码页超时,请检查邮箱提交后页面是否仍停留在官网或邮箱页。');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await ensureContentScriptReadyOnTab('signup-page', tabId, {
|
||||||
|
inject: SIGNUP_PAGE_INJECT_FILES,
|
||||||
|
injectSource: 'signup-page',
|
||||||
|
timeoutMs: 45000,
|
||||||
|
retryDelayMs: 900,
|
||||||
|
logMessage: `步骤 ${step}:密码页仍在加载,正在重试连接内容脚本...`,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await sendToContentScriptResilient('signup-page', {
|
||||||
|
type: 'ENSURE_SIGNUP_PASSWORD_PAGE_READY',
|
||||||
|
step,
|
||||||
|
source: 'background',
|
||||||
|
payload: {},
|
||||||
|
}, {
|
||||||
|
timeoutMs: 20000,
|
||||||
|
retryDelayMs: 700,
|
||||||
|
logMessage: `步骤 ${step}:认证页正在切换,等待密码页重新就绪...`,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result?.error) {
|
||||||
|
throw new Error(result.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result || {};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resolveSignupEmailForFlow(state) {
|
||||||
|
let resolvedEmail = state.email;
|
||||||
|
if (isHotmailProvider(state)) {
|
||||||
|
const account = await ensureHotmailAccountForFlow({
|
||||||
|
allowAllocate: true,
|
||||||
|
markUsed: true,
|
||||||
|
preferredAccountId: state.currentHotmailAccountId || null,
|
||||||
|
});
|
||||||
|
resolvedEmail = account.email;
|
||||||
|
} else if (isLuckmailProvider(state)) {
|
||||||
|
const purchase = await ensureLuckmailPurchaseForFlow({ allowReuse: true });
|
||||||
|
resolvedEmail = purchase.email_address;
|
||||||
|
} else if (isGeneratedAliasProvider(state)) {
|
||||||
|
resolvedEmail = buildGeneratedAliasEmail(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!resolvedEmail) {
|
||||||
|
throw new Error('缺少邮箱地址,请先在侧边栏粘贴邮箱。');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resolvedEmail !== state.email) {
|
||||||
|
await setEmailState(resolvedEmail);
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolvedEmail;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
ensureSignupEntryPageReady,
|
||||||
|
ensureSignupPasswordPageReadyInTab,
|
||||||
|
openSignupEntryTab,
|
||||||
|
resolveSignupEmailForFlow,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
createSignupFlowHelpers,
|
||||||
|
};
|
||||||
|
});
|
||||||
@@ -0,0 +1,189 @@
|
|||||||
|
# 2026-04-16 Architecture Refactor Plan
|
||||||
|
|
||||||
|
## 目标
|
||||||
|
|
||||||
|
在不破坏现有 1~9 步自动化流程、消息流、状态机和测试保护面的前提下,逐步拆分当前超大的主文件,降低后续开发和排障成本。
|
||||||
|
|
||||||
|
## 当前事实
|
||||||
|
|
||||||
|
- 这是一个无构建步骤的 Manifest V3 扩展。
|
||||||
|
- `background.js` 是 classic service worker,当前依赖 `importScripts(...)`。
|
||||||
|
- `sidepanel/sidepanel.html` 通过多个 `<script>` 直接加载脚本。
|
||||||
|
- 运行态核心链路依赖:
|
||||||
|
- `chrome.storage.session` 保存流程态
|
||||||
|
- `chrome.storage.local` 保存配置态
|
||||||
|
- `chrome.runtime.sendMessage` 贯穿 sidepanel / background / content scripts
|
||||||
|
- `chrome.tabs` / `chrome.webNavigation` / `chrome.debugger` 支撑步骤执行
|
||||||
|
- 测试不仅验证行为,还会直接从 `background.js` / `sidepanel.js` / `content/*.js` 按函数名提取源码执行。
|
||||||
|
|
||||||
|
## 已确认基线
|
||||||
|
|
||||||
|
- `bun test` 当前基线:`84 pass / 0 fail`
|
||||||
|
- 关键大文件规模:
|
||||||
|
- `background.js`: 7481 行
|
||||||
|
- `sidepanel/sidepanel.js`: 4531 行
|
||||||
|
- `content/signup-page.js`: 1969 行
|
||||||
|
|
||||||
|
## 当前结构压力点
|
||||||
|
|
||||||
|
### `background.js`
|
||||||
|
|
||||||
|
- 状态/配置、邮箱提供者、Tab 管理、消息路由、自动运行状态机、步骤 1~9 全部集中在单文件。
|
||||||
|
- 文件内同时存在:
|
||||||
|
- 纯函数
|
||||||
|
- 网络请求
|
||||||
|
- 存储读写
|
||||||
|
- 浏览器 API 调度
|
||||||
|
- 步骤业务逻辑
|
||||||
|
- 风险最高的区域不是“代码长”,而是“跨区域共享状态太多”。
|
||||||
|
|
||||||
|
### `sidepanel/sidepanel.js`
|
||||||
|
|
||||||
|
- 同时承担:
|
||||||
|
- DOM 查询
|
||||||
|
- 状态同步
|
||||||
|
- 配置读写
|
||||||
|
- 多个 provider 管理器
|
||||||
|
- 按钮事件绑定
|
||||||
|
- background 广播处理
|
||||||
|
- Hotmail / LuckMail / iCloud 三块都已经是天然的 feature slice,但仍堆在一个文件里。
|
||||||
|
|
||||||
|
## 重构硬约束
|
||||||
|
|
||||||
|
### 约束 1:先拆“强内聚功能块”,后拆“流程主干”
|
||||||
|
|
||||||
|
优先拆:
|
||||||
|
|
||||||
|
- Hotmail 账号池 UI
|
||||||
|
- LuckMail 管理 UI
|
||||||
|
- iCloud 别名管理 UI
|
||||||
|
- CPA / SUB2API 面板桥接层
|
||||||
|
|
||||||
|
暂缓拆:
|
||||||
|
|
||||||
|
- `executeStep1~9`
|
||||||
|
- `autoRunLoop`
|
||||||
|
- `handleMessage`
|
||||||
|
- `resolveVerificationStep`
|
||||||
|
|
||||||
|
原因:这些主干函数既是运行核心,也是当前测试抽取最密集的区域。
|
||||||
|
|
||||||
|
### 约束 2:第一阶段尽量不改变被测试函数的“定义位置”
|
||||||
|
|
||||||
|
由于现有测试会直接从源文件提取函数源码,第一阶段要避免大规模把被测函数从原文件移走。
|
||||||
|
|
||||||
|
### 约束 3:运行边界不能变
|
||||||
|
|
||||||
|
以下运行边界必须保持:
|
||||||
|
|
||||||
|
- `background.js` 继续是 service worker 入口
|
||||||
|
- `sidepanel/sidepanel.html` 继续是 sidepanel 入口
|
||||||
|
- content script 注入方式不变
|
||||||
|
- 所有 runtime message type 不变
|
||||||
|
- storage key 不变
|
||||||
|
|
||||||
|
## 目标结构
|
||||||
|
|
||||||
|
### 背景层目标
|
||||||
|
|
||||||
|
```txt
|
||||||
|
background.js
|
||||||
|
background/
|
||||||
|
state/
|
||||||
|
settings.js
|
||||||
|
session-state.js
|
||||||
|
runtime/
|
||||||
|
tabs.js
|
||||||
|
command-queue.js
|
||||||
|
logging.js
|
||||||
|
message-router.js
|
||||||
|
providers/
|
||||||
|
hotmail.js
|
||||||
|
luckmail.js
|
||||||
|
icloud.js
|
||||||
|
generated-email.js
|
||||||
|
steps/
|
||||||
|
step1.js
|
||||||
|
step2.js
|
||||||
|
step3.js
|
||||||
|
verification.js
|
||||||
|
step5.js
|
||||||
|
step6.js
|
||||||
|
step7.js
|
||||||
|
step8.js
|
||||||
|
step9.js
|
||||||
|
auto-run/
|
||||||
|
scheduler.js
|
||||||
|
loop.js
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sidepanel 层目标
|
||||||
|
|
||||||
|
```txt
|
||||||
|
sidepanel/sidepanel.js
|
||||||
|
sidepanel/
|
||||||
|
hotmail-manager.js
|
||||||
|
luckmail-manager.js
|
||||||
|
icloud-manager.js
|
||||||
|
auto-run-ui.js
|
||||||
|
runtime-listeners.js
|
||||||
|
update-service.js
|
||||||
|
```
|
||||||
|
|
||||||
|
## 分阶段执行顺序
|
||||||
|
|
||||||
|
### Phase 1
|
||||||
|
|
||||||
|
先拆 `sidepanel` 的强内聚管理区块。
|
||||||
|
|
||||||
|
- 先拆 Hotmail manager
|
||||||
|
- 再拆 iCloud manager
|
||||||
|
- 再拆 LuckMail manager
|
||||||
|
|
||||||
|
原因:
|
||||||
|
|
||||||
|
- 不触碰主流程状态机
|
||||||
|
- 风险主要局限在 sidepanel UI
|
||||||
|
- 可以验证“多脚本 + 工厂上下文”的拆分模式
|
||||||
|
|
||||||
|
### Phase 2
|
||||||
|
|
||||||
|
拆 `background.js` 中和主流程相对解耦的桥接层。
|
||||||
|
|
||||||
|
- CPA / SUB2API OAuth 桥接
|
||||||
|
- 邮箱 provider 的 API 访问层
|
||||||
|
- Cloudflare / Duck / iCloud 生成邮箱层
|
||||||
|
|
||||||
|
### Phase 3
|
||||||
|
|
||||||
|
拆 `background.js` 中的运行时基础设施。
|
||||||
|
|
||||||
|
- tab registry
|
||||||
|
- command queue
|
||||||
|
- logging
|
||||||
|
- storage helpers
|
||||||
|
|
||||||
|
### Phase 4
|
||||||
|
|
||||||
|
最后拆步骤执行器与 auto-run 主循环。
|
||||||
|
|
||||||
|
- `executeStep1~9`
|
||||||
|
- `resolveVerificationStep`
|
||||||
|
- `autoRunLoop`
|
||||||
|
|
||||||
|
## 第一阶段落地策略
|
||||||
|
|
||||||
|
本轮先做:
|
||||||
|
|
||||||
|
1. 新增 `sidepanel/hotmail-manager.js`
|
||||||
|
2. 让 `sidepanel.js` 通过工厂方式接入 Hotmail manager
|
||||||
|
3. 不改变 runtime message type
|
||||||
|
4. 不改变 Hotmail 区块的 UI 行为
|
||||||
|
5. 跑现有测试确认基线仍然成立
|
||||||
|
|
||||||
|
## 验收标准
|
||||||
|
|
||||||
|
- `bun test` 继续全绿
|
||||||
|
- sidepanel Hotmail 区块行为无回归
|
||||||
|
- `sidepanel.js` 不再直接承载 Hotmail 账号池的大段渲染和事件逻辑
|
||||||
|
- 后续可以按同样模式继续拆 iCloud / LuckMail
|
||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "多页面自动化",
|
"name": "多页面自动化",
|
||||||
"version": "10.2.0",
|
"version": "10.4.0",
|
||||||
"description": "用于自动执行多步骤 OAuth 注册流程",
|
"description": "用于自动执行多步骤 OAuth 注册流程",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"sidePanel",
|
"sidePanel",
|
||||||
|
|||||||
@@ -0,0 +1,529 @@
|
|||||||
|
(function attachSidepanelHotmailManager(globalScope) {
|
||||||
|
function createHotmailManager(context = {}) {
|
||||||
|
const {
|
||||||
|
state,
|
||||||
|
dom,
|
||||||
|
helpers,
|
||||||
|
runtime,
|
||||||
|
constants = {},
|
||||||
|
hotmailUtils = {},
|
||||||
|
} = context;
|
||||||
|
|
||||||
|
const expandedStorageKey = constants.expandedStorageKey || 'multipage-hotmail-list-expanded';
|
||||||
|
const displayTimeZone = constants.displayTimeZone || 'Asia/Shanghai';
|
||||||
|
const copyIcon = constants.copyIcon || '';
|
||||||
|
|
||||||
|
let actionInFlight = false;
|
||||||
|
let listExpanded = false;
|
||||||
|
|
||||||
|
function getHotmailAccountsByUsage(mode = 'all', currentState = state.getLatestState()) {
|
||||||
|
const accounts = helpers.getHotmailAccounts(currentState);
|
||||||
|
if (typeof hotmailUtils.filterHotmailAccountsByUsage === 'function') {
|
||||||
|
return hotmailUtils.filterHotmailAccountsByUsage(accounts, mode);
|
||||||
|
}
|
||||||
|
if (mode === 'used') {
|
||||||
|
return accounts.filter((account) => Boolean(account?.used));
|
||||||
|
}
|
||||||
|
return accounts.slice();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getHotmailBulkActionText(mode, count) {
|
||||||
|
if (typeof hotmailUtils.getHotmailBulkActionLabel === 'function') {
|
||||||
|
return hotmailUtils.getHotmailBulkActionLabel(mode, count);
|
||||||
|
}
|
||||||
|
const normalizedCount = Number.isFinite(Number(count)) ? Math.max(0, Number(count)) : 0;
|
||||||
|
const prefix = mode === 'used' ? '清空已用' : '全部删除';
|
||||||
|
const suffix = normalizedCount > 0 ? `(${normalizedCount})` : '';
|
||||||
|
return `${prefix}${suffix}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getHotmailListToggleText(expanded, count) {
|
||||||
|
if (typeof hotmailUtils.getHotmailListToggleLabel === 'function') {
|
||||||
|
return hotmailUtils.getHotmailListToggleLabel(expanded, count);
|
||||||
|
}
|
||||||
|
const normalizedCount = Number.isFinite(Number(count)) ? Math.max(0, Number(count)) : 0;
|
||||||
|
const suffix = normalizedCount > 0 ? `(${normalizedCount})` : '';
|
||||||
|
return `${expanded ? '收起列表' : '展开列表'}${suffix}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateHotmailListViewport() {
|
||||||
|
const count = helpers.getHotmailAccounts().length;
|
||||||
|
const usedCount = getHotmailAccountsByUsage('used').length;
|
||||||
|
if (dom.btnClearUsedHotmailAccounts) {
|
||||||
|
dom.btnClearUsedHotmailAccounts.textContent = getHotmailBulkActionText('used', usedCount);
|
||||||
|
dom.btnClearUsedHotmailAccounts.disabled = usedCount === 0;
|
||||||
|
}
|
||||||
|
if (dom.btnDeleteAllHotmailAccounts) {
|
||||||
|
dom.btnDeleteAllHotmailAccounts.textContent = getHotmailBulkActionText('all', count);
|
||||||
|
dom.btnDeleteAllHotmailAccounts.disabled = count === 0;
|
||||||
|
}
|
||||||
|
if (dom.btnToggleHotmailList) {
|
||||||
|
dom.btnToggleHotmailList.textContent = getHotmailListToggleText(listExpanded, count);
|
||||||
|
dom.btnToggleHotmailList.setAttribute('aria-expanded', String(listExpanded));
|
||||||
|
dom.btnToggleHotmailList.disabled = count === 0;
|
||||||
|
}
|
||||||
|
if (dom.hotmailListShell) {
|
||||||
|
dom.hotmailListShell.classList.toggle('is-expanded', listExpanded);
|
||||||
|
dom.hotmailListShell.classList.toggle('is-collapsed', !listExpanded);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setHotmailListExpanded(expanded, options = {}) {
|
||||||
|
const { persist = true } = options;
|
||||||
|
listExpanded = Boolean(expanded);
|
||||||
|
updateHotmailListViewport();
|
||||||
|
if (persist) {
|
||||||
|
localStorage.setItem(expandedStorageKey, listExpanded ? '1' : '0');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function initHotmailListExpandedState() {
|
||||||
|
const saved = localStorage.getItem(expandedStorageKey);
|
||||||
|
setHotmailListExpanded(saved === '1', { persist: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldClearCurrentHotmailSelectionLocally(account) {
|
||||||
|
if (typeof hotmailUtils.shouldClearHotmailCurrentSelection === 'function') {
|
||||||
|
return hotmailUtils.shouldClearHotmailCurrentSelection(account);
|
||||||
|
}
|
||||||
|
return Boolean(account) && account.used === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function upsertHotmailAccountListLocally(accounts, nextAccount) {
|
||||||
|
if (typeof hotmailUtils.upsertHotmailAccountInList === 'function') {
|
||||||
|
return hotmailUtils.upsertHotmailAccountInList(accounts, nextAccount);
|
||||||
|
}
|
||||||
|
|
||||||
|
const list = Array.isArray(accounts) ? accounts.slice() : [];
|
||||||
|
if (!nextAccount?.id) return list;
|
||||||
|
|
||||||
|
const existingIndex = list.findIndex((account) => account?.id === nextAccount.id);
|
||||||
|
if (existingIndex === -1) {
|
||||||
|
list.push(nextAccount);
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
list[existingIndex] = nextAccount;
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshHotmailSelectionUI() {
|
||||||
|
renderHotmailAccounts();
|
||||||
|
if (dom.selectMailProvider.value === 'hotmail-api') {
|
||||||
|
dom.inputEmail.value = helpers.getCurrentHotmailEmail();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyHotmailAccountMutation(account, options = {}) {
|
||||||
|
if (!account?.id) return;
|
||||||
|
const { preserveCurrentSelection = false } = options;
|
||||||
|
|
||||||
|
const latestState = state.getLatestState();
|
||||||
|
const nextState = {
|
||||||
|
hotmailAccounts: upsertHotmailAccountListLocally(helpers.getHotmailAccounts(), account),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!preserveCurrentSelection
|
||||||
|
&& latestState?.currentHotmailAccountId === account.id
|
||||||
|
&& shouldClearCurrentHotmailSelectionLocally(account)) {
|
||||||
|
nextState.currentHotmailAccountId = null;
|
||||||
|
if (dom.selectMailProvider.value === 'hotmail-api') {
|
||||||
|
nextState.email = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
state.syncLatestState(nextState);
|
||||||
|
refreshHotmailSelectionUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDateTime(timestamp) {
|
||||||
|
const value = Number(timestamp);
|
||||||
|
if (!Number.isFinite(value) || value <= 0) {
|
||||||
|
return '未使用';
|
||||||
|
}
|
||||||
|
return new Date(value).toLocaleString('zh-CN', {
|
||||||
|
hour12: false,
|
||||||
|
timeZone: displayTimeZone,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getHotmailAvailabilityLabel(account) {
|
||||||
|
if (account.used) return '已用';
|
||||||
|
return '可分配';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getHotmailStatusLabel(account) {
|
||||||
|
if (account.used) return '已用';
|
||||||
|
|
||||||
|
switch (account.status) {
|
||||||
|
case 'authorized':
|
||||||
|
return '可用';
|
||||||
|
case 'error':
|
||||||
|
return '异常';
|
||||||
|
default:
|
||||||
|
return '待校验';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getHotmailStatusClass(account) {
|
||||||
|
if (account.used) return 'status-used';
|
||||||
|
return `status-${account.status || 'pending'}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearHotmailForm() {
|
||||||
|
dom.inputHotmailEmail.value = '';
|
||||||
|
dom.inputHotmailClientId.value = '';
|
||||||
|
dom.inputHotmailPassword.value = '';
|
||||||
|
dom.inputHotmailRefreshToken.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderHotmailAccounts() {
|
||||||
|
if (!dom.hotmailAccountsList) return;
|
||||||
|
const latestState = state.getLatestState();
|
||||||
|
const accounts = helpers.getHotmailAccounts();
|
||||||
|
const currentId = latestState?.currentHotmailAccountId || '';
|
||||||
|
|
||||||
|
if (!accounts.length) {
|
||||||
|
dom.hotmailAccountsList.innerHTML = '<div class="hotmail-empty">还没有 Hotmail 账号,先添加一条再校验。</div>';
|
||||||
|
updateHotmailListViewport();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dom.hotmailAccountsList.innerHTML = accounts.map((account) => `
|
||||||
|
<div class="hotmail-account-item${account.id === currentId ? ' is-current' : ''}">
|
||||||
|
<div class="hotmail-account-top">
|
||||||
|
<div class="hotmail-account-title-row">
|
||||||
|
<div class="hotmail-account-email">${helpers.escapeHtml(account.email || '(未命名账号)')}</div>
|
||||||
|
<button
|
||||||
|
class="hotmail-copy-btn"
|
||||||
|
type="button"
|
||||||
|
data-account-action="copy-email"
|
||||||
|
data-account-id="${helpers.escapeHtml(account.id)}"
|
||||||
|
title="复制邮箱"
|
||||||
|
aria-label="复制邮箱 ${helpers.escapeHtml(account.email || '')}"
|
||||||
|
>${copyIcon}</button>
|
||||||
|
</div>
|
||||||
|
<span class="hotmail-status-chip ${helpers.escapeHtml(getHotmailStatusClass(account))}">${helpers.escapeHtml(getHotmailStatusLabel(account))}</span>
|
||||||
|
</div>
|
||||||
|
<div class="hotmail-account-meta">
|
||||||
|
<span>客户端 ID:${helpers.escapeHtml(account.clientId ? `${account.clientId.slice(0, 10)}...` : '未填写')}</span>
|
||||||
|
<span>刷新令牌:${account.refreshToken ? '已保存' : '未保存'}</span>
|
||||||
|
<span>分配状态: ${helpers.escapeHtml(getHotmailAvailabilityLabel(account))}</span>
|
||||||
|
<span>上次校验: ${helpers.escapeHtml(formatDateTime(account.lastAuthAt))}</span>
|
||||||
|
<span>上次使用: ${helpers.escapeHtml(formatDateTime(account.lastUsedAt))}</span>
|
||||||
|
</div>
|
||||||
|
${account.lastError ? `<div class="hotmail-account-error">${helpers.escapeHtml(account.lastError)}</div>` : ''}
|
||||||
|
<div class="hotmail-account-actions">
|
||||||
|
<button class="btn btn-outline btn-sm" type="button" data-account-action="select" data-account-id="${helpers.escapeHtml(account.id)}">使用此账号</button>
|
||||||
|
<button class="btn btn-outline btn-sm" type="button" data-account-action="toggle-used" data-account-id="${helpers.escapeHtml(account.id)}">${account.used ? '标记未用' : '标记已用'}</button>
|
||||||
|
<button class="btn btn-primary btn-sm" type="button" data-account-action="verify" data-account-id="${helpers.escapeHtml(account.id)}">校验</button>
|
||||||
|
<button class="btn btn-outline btn-sm" type="button" data-account-action="test" data-account-id="${helpers.escapeHtml(account.id)}">复制最新验证码</button>
|
||||||
|
<button class="btn btn-ghost btn-sm" type="button" data-account-action="delete" data-account-id="${helpers.escapeHtml(account.id)}">删除</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
updateHotmailListViewport();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteHotmailAccountsByMode(mode) {
|
||||||
|
const isUsedMode = mode === 'used';
|
||||||
|
const targetAccounts = getHotmailAccountsByUsage(isUsedMode ? 'used' : 'all');
|
||||||
|
if (!targetAccounts.length) {
|
||||||
|
helpers.showToast(isUsedMode ? '没有已用账号可清空。' : '没有可删除的 Hotmail 账号。', 'warn');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirmed = await helpers.openConfirmModal({
|
||||||
|
title: isUsedMode ? '清空已用账号' : '全部删除账号',
|
||||||
|
message: isUsedMode
|
||||||
|
? `确认删除当前 ${targetAccounts.length} 个已用 Hotmail 账号吗?`
|
||||||
|
: `确认删除全部 ${targetAccounts.length} 个 Hotmail 账号吗?`,
|
||||||
|
confirmLabel: isUsedMode ? '确认清空已用' : '确认全部删除',
|
||||||
|
confirmVariant: isUsedMode ? 'btn-outline' : 'btn-danger',
|
||||||
|
});
|
||||||
|
if (!confirmed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await runtime.sendMessage({
|
||||||
|
type: 'DELETE_HOTMAIL_ACCOUNTS',
|
||||||
|
source: 'sidepanel',
|
||||||
|
payload: { mode: isUsedMode ? 'used' : 'all' },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response?.error) {
|
||||||
|
throw new Error(response.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const latestState = state.getLatestState();
|
||||||
|
const targetIds = new Set(targetAccounts.map((account) => account.id));
|
||||||
|
const nextAccounts = isUsedMode
|
||||||
|
? helpers.getHotmailAccounts().filter((account) => !targetIds.has(account.id))
|
||||||
|
: [];
|
||||||
|
const nextState = { hotmailAccounts: nextAccounts };
|
||||||
|
if (latestState?.currentHotmailAccountId && targetIds.has(latestState.currentHotmailAccountId)) {
|
||||||
|
nextState.currentHotmailAccountId = null;
|
||||||
|
if (dom.selectMailProvider.value === 'hotmail-api') {
|
||||||
|
nextState.email = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
state.syncLatestState(nextState);
|
||||||
|
refreshHotmailSelectionUI();
|
||||||
|
|
||||||
|
helpers.showToast(
|
||||||
|
isUsedMode
|
||||||
|
? `已清空 ${response.deletedCount || 0} 个已用 Hotmail 账号`
|
||||||
|
: `已删除全部 ${response.deletedCount || 0} 个 Hotmail 账号`,
|
||||||
|
'success',
|
||||||
|
2200
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleAddHotmailAccount() {
|
||||||
|
if (actionInFlight) return;
|
||||||
|
|
||||||
|
const email = dom.inputHotmailEmail.value.trim();
|
||||||
|
const clientId = dom.inputHotmailClientId.value.trim();
|
||||||
|
const refreshToken = dom.inputHotmailRefreshToken.value.trim();
|
||||||
|
if (!email) {
|
||||||
|
helpers.showToast('请先填写 Hotmail 邮箱。', 'warn');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!clientId) {
|
||||||
|
helpers.showToast('请先填写微软应用客户端 ID。', 'warn');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!refreshToken) {
|
||||||
|
helpers.showToast('请先填写刷新令牌(refresh token)。', 'warn');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
actionInFlight = true;
|
||||||
|
dom.btnAddHotmailAccount.disabled = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await runtime.sendMessage({
|
||||||
|
type: 'UPSERT_HOTMAIL_ACCOUNT',
|
||||||
|
source: 'sidepanel',
|
||||||
|
payload: {
|
||||||
|
email,
|
||||||
|
clientId,
|
||||||
|
password: dom.inputHotmailPassword.value,
|
||||||
|
refreshToken,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response?.error) {
|
||||||
|
throw new Error(response.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
helpers.showToast(`已保存 Hotmail 账号 ${email}`, 'success', 1800);
|
||||||
|
clearHotmailForm();
|
||||||
|
} catch (err) {
|
||||||
|
helpers.showToast(`保存 Hotmail 账号失败:${err.message}`, 'error');
|
||||||
|
} finally {
|
||||||
|
actionInFlight = false;
|
||||||
|
dom.btnAddHotmailAccount.disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleImportHotmailAccounts() {
|
||||||
|
if (actionInFlight) return;
|
||||||
|
if (typeof hotmailUtils.parseHotmailImportText !== 'function') {
|
||||||
|
helpers.showToast('导入解析器未加载,请刷新扩展后重试。', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawText = dom.inputHotmailImport.value.trim();
|
||||||
|
if (!rawText) {
|
||||||
|
helpers.showToast('请先粘贴账号导入内容。', 'warn');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedAccounts = hotmailUtils.parseHotmailImportText(rawText);
|
||||||
|
if (!parsedAccounts.length) {
|
||||||
|
helpers.showToast('没有解析到有效账号,请检查格式是否为 账号----密码----ID----Token。', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
actionInFlight = true;
|
||||||
|
dom.btnImportHotmailAccounts.disabled = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (const account of parsedAccounts) {
|
||||||
|
const response = await runtime.sendMessage({
|
||||||
|
type: 'UPSERT_HOTMAIL_ACCOUNT',
|
||||||
|
source: 'sidepanel',
|
||||||
|
payload: account,
|
||||||
|
});
|
||||||
|
if (response?.error) {
|
||||||
|
throw new Error(response.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dom.inputHotmailImport.value = '';
|
||||||
|
helpers.showToast(`已导入 ${parsedAccounts.length} 条 Hotmail 账号`, 'success', 2200);
|
||||||
|
} catch (err) {
|
||||||
|
helpers.showToast(`批量导入失败:${err.message}`, 'error');
|
||||||
|
} finally {
|
||||||
|
actionInFlight = false;
|
||||||
|
dom.btnImportHotmailAccounts.disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleAccountListClick(event) {
|
||||||
|
const actionButton = event.target.closest('[data-account-action]');
|
||||||
|
if (!actionButton || actionInFlight) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const accountId = actionButton.dataset.accountId;
|
||||||
|
const action = actionButton.dataset.accountAction;
|
||||||
|
if (!accountId || !action) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetAccount = helpers.getHotmailAccounts().find((account) => account.id === accountId) || null;
|
||||||
|
|
||||||
|
actionInFlight = true;
|
||||||
|
actionButton.disabled = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (action === 'copy-email') {
|
||||||
|
if (!targetAccount?.email) throw new Error('未找到可复制的邮箱地址。');
|
||||||
|
await helpers.copyTextToClipboard(targetAccount.email);
|
||||||
|
helpers.showToast(`已复制 ${targetAccount.email}`, 'success', 1800);
|
||||||
|
} else if (action === 'select') {
|
||||||
|
const response = await runtime.sendMessage({
|
||||||
|
type: 'SELECT_HOTMAIL_ACCOUNT',
|
||||||
|
source: 'sidepanel',
|
||||||
|
payload: { accountId },
|
||||||
|
});
|
||||||
|
if (response?.error) throw new Error(response.error);
|
||||||
|
state.syncLatestState({ currentHotmailAccountId: response.account.id });
|
||||||
|
applyHotmailAccountMutation(response.account, { preserveCurrentSelection: true });
|
||||||
|
helpers.showToast(`已切换当前 Hotmail 账号为 ${response.account.email}`, 'success', 1800);
|
||||||
|
} else if (action === 'toggle-used') {
|
||||||
|
if (!targetAccount) throw new Error('未找到目标 Hotmail 账号。');
|
||||||
|
const response = await runtime.sendMessage({
|
||||||
|
type: 'PATCH_HOTMAIL_ACCOUNT',
|
||||||
|
source: 'sidepanel',
|
||||||
|
payload: {
|
||||||
|
accountId,
|
||||||
|
updates: { used: !targetAccount.used },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (response?.error) throw new Error(response.error);
|
||||||
|
applyHotmailAccountMutation(response.account);
|
||||||
|
helpers.showToast(`账号 ${response.account.email} 已${response.account.used ? '标记为已用' : '恢复为未用'}`, 'success', 2200);
|
||||||
|
} else if (action === 'verify') {
|
||||||
|
const response = await runtime.sendMessage({
|
||||||
|
type: 'VERIFY_HOTMAIL_ACCOUNT',
|
||||||
|
source: 'sidepanel',
|
||||||
|
payload: { accountId },
|
||||||
|
});
|
||||||
|
if (response?.error) throw new Error(response.error);
|
||||||
|
applyHotmailAccountMutation(response.account, { preserveCurrentSelection: true });
|
||||||
|
helpers.showToast(`账号 ${response.account.email} 校验通过`, 'success', 2200);
|
||||||
|
} else if (action === 'test') {
|
||||||
|
const response = await runtime.sendMessage({
|
||||||
|
type: 'TEST_HOTMAIL_ACCOUNT',
|
||||||
|
source: 'sidepanel',
|
||||||
|
payload: { accountId },
|
||||||
|
});
|
||||||
|
if (response?.error) throw new Error(response.error);
|
||||||
|
applyHotmailAccountMutation(response.account, { preserveCurrentSelection: true });
|
||||||
|
if (response.latestCode) {
|
||||||
|
await helpers.copyTextToClipboard(response.latestCode);
|
||||||
|
const mailbox = response.latestMailbox ? `(${response.latestMailbox})` : '';
|
||||||
|
helpers.showToast(`已复制最新验证码 ${response.latestCode}${mailbox}`, 'success', 2600);
|
||||||
|
} else if (response.latestSubject) {
|
||||||
|
const mailbox = response.latestMailbox ? `(${response.latestMailbox})` : '';
|
||||||
|
helpers.showToast(`最新邮件${mailbox}没有验证码:${response.latestSubject}`, 'warn', 3200);
|
||||||
|
} else {
|
||||||
|
helpers.showToast('当前没有可读取的最新邮件。', 'warn', 2600);
|
||||||
|
}
|
||||||
|
} else if (action === 'delete') {
|
||||||
|
const confirmed = await helpers.openConfirmModal({
|
||||||
|
title: '删除账号',
|
||||||
|
message: '确认删除这个 Hotmail 账号吗?对应 token 也会一起移除。',
|
||||||
|
confirmLabel: '确认删除',
|
||||||
|
confirmVariant: 'btn-danger',
|
||||||
|
});
|
||||||
|
if (!confirmed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const response = await runtime.sendMessage({
|
||||||
|
type: 'DELETE_HOTMAIL_ACCOUNT',
|
||||||
|
source: 'sidepanel',
|
||||||
|
payload: { accountId },
|
||||||
|
});
|
||||||
|
if (response?.error) throw new Error(response.error);
|
||||||
|
helpers.showToast('Hotmail 账号已删除', 'success', 1800);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
helpers.showToast(err.message, 'error');
|
||||||
|
} finally {
|
||||||
|
actionInFlight = false;
|
||||||
|
actionButton.disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindHotmailEvents() {
|
||||||
|
dom.btnToggleHotmailList?.addEventListener('click', () => {
|
||||||
|
setHotmailListExpanded(!listExpanded);
|
||||||
|
});
|
||||||
|
|
||||||
|
dom.btnHotmailUsageGuide?.addEventListener('click', async () => {
|
||||||
|
await helpers.openConfirmModal({
|
||||||
|
title: '使用教程',
|
||||||
|
message: 'API对接模式会直接调用微软邮箱接口取件;本地助手模式仍走本地服务。两种模式继续共用同一套 Hotmail 账号池与导入格式。',
|
||||||
|
confirmLabel: '确定',
|
||||||
|
confirmVariant: 'btn-primary',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
dom.btnClearUsedHotmailAccounts?.addEventListener('click', async () => {
|
||||||
|
if (actionInFlight) return;
|
||||||
|
actionInFlight = true;
|
||||||
|
dom.btnClearUsedHotmailAccounts.disabled = true;
|
||||||
|
try {
|
||||||
|
await deleteHotmailAccountsByMode('used');
|
||||||
|
} catch (err) {
|
||||||
|
helpers.showToast(err.message, 'error');
|
||||||
|
} finally {
|
||||||
|
actionInFlight = false;
|
||||||
|
updateHotmailListViewport();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
dom.btnDeleteAllHotmailAccounts?.addEventListener('click', async () => {
|
||||||
|
if (actionInFlight) return;
|
||||||
|
actionInFlight = true;
|
||||||
|
dom.btnDeleteAllHotmailAccounts.disabled = true;
|
||||||
|
try {
|
||||||
|
await deleteHotmailAccountsByMode('all');
|
||||||
|
} catch (err) {
|
||||||
|
helpers.showToast(err.message, 'error');
|
||||||
|
} finally {
|
||||||
|
actionInFlight = false;
|
||||||
|
updateHotmailListViewport();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
dom.btnAddHotmailAccount?.addEventListener('click', handleAddHotmailAccount);
|
||||||
|
dom.btnImportHotmailAccounts?.addEventListener('click', handleImportHotmailAccounts);
|
||||||
|
dom.hotmailAccountsList?.addEventListener('click', handleAccountListClick);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
bindHotmailEvents,
|
||||||
|
initHotmailListExpandedState,
|
||||||
|
renderHotmailAccounts,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
globalScope.SidepanelHotmailManager = {
|
||||||
|
createHotmailManager,
|
||||||
|
};
|
||||||
|
})(window);
|
||||||
@@ -0,0 +1,502 @@
|
|||||||
|
(function attachSidepanelIcloudManager(globalScope) {
|
||||||
|
function createIcloudManager(context = {}) {
|
||||||
|
const {
|
||||||
|
dom,
|
||||||
|
helpers,
|
||||||
|
runtime,
|
||||||
|
} = context;
|
||||||
|
|
||||||
|
let refreshQueued = false;
|
||||||
|
let renderedAliases = [];
|
||||||
|
let selectedEmails = new Set();
|
||||||
|
let searchTerm = '';
|
||||||
|
let filterMode = 'all';
|
||||||
|
|
||||||
|
function normalizeIcloudSearchText(value) {
|
||||||
|
return String(value || '').trim().toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFilteredIcloudAliases(aliases = renderedAliases) {
|
||||||
|
const normalizedSearchTerm = normalizeIcloudSearchText(searchTerm);
|
||||||
|
return (Array.isArray(aliases) ? aliases : []).filter((alias) => {
|
||||||
|
const matchesFilter = (() => {
|
||||||
|
switch (filterMode) {
|
||||||
|
case 'active': return Boolean(alias.active);
|
||||||
|
case 'used': return Boolean(alias.used);
|
||||||
|
case 'unused': return !alias.used;
|
||||||
|
case 'preserved': return Boolean(alias.preserved);
|
||||||
|
default: return true;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
if (!matchesFilter) return false;
|
||||||
|
if (!normalizedSearchTerm) return true;
|
||||||
|
|
||||||
|
const haystack = [
|
||||||
|
alias.email,
|
||||||
|
alias.label,
|
||||||
|
alias.note,
|
||||||
|
alias.used ? '已用 used' : '未用 unused',
|
||||||
|
alias.active ? '可用 active' : '不可用 inactive',
|
||||||
|
alias.preserved ? '保留 preserved' : '',
|
||||||
|
].join(' ').toLowerCase();
|
||||||
|
|
||||||
|
return haystack.includes(normalizedSearchTerm);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function pruneIcloudSelection(aliases = renderedAliases) {
|
||||||
|
const existing = new Set((Array.isArray(aliases) ? aliases : []).map((alias) => alias.email));
|
||||||
|
selectedEmails = new Set([...selectedEmails].filter((email) => existing.has(email)));
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateIcloudBulkUI(visibleAliases = getFilteredIcloudAliases()) {
|
||||||
|
if (!dom.checkboxIcloudSelectAll || !dom.icloudSelectionSummary) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const visibleEmails = visibleAliases.map((alias) => alias.email);
|
||||||
|
const selectedVisibleCount = visibleEmails.filter((email) => selectedEmails.has(email)).length;
|
||||||
|
const hasVisible = visibleEmails.length > 0;
|
||||||
|
|
||||||
|
dom.checkboxIcloudSelectAll.checked = hasVisible && selectedVisibleCount === visibleEmails.length;
|
||||||
|
dom.checkboxIcloudSelectAll.indeterminate = selectedVisibleCount > 0 && selectedVisibleCount < visibleEmails.length;
|
||||||
|
dom.checkboxIcloudSelectAll.disabled = !hasVisible;
|
||||||
|
dom.icloudSelectionSummary.textContent = `已选 ${selectedEmails.size} 个(当前显示 ${visibleEmails.length} 个)`;
|
||||||
|
|
||||||
|
const hasSelection = selectedEmails.size > 0;
|
||||||
|
if (dom.btnIcloudBulkUsed) dom.btnIcloudBulkUsed.disabled = !hasSelection;
|
||||||
|
if (dom.btnIcloudBulkUnused) dom.btnIcloudBulkUnused.disabled = !hasSelection;
|
||||||
|
if (dom.btnIcloudBulkPreserve) dom.btnIcloudBulkPreserve.disabled = !hasSelection;
|
||||||
|
if (dom.btnIcloudBulkUnpreserve) dom.btnIcloudBulkUnpreserve.disabled = !hasSelection;
|
||||||
|
if (dom.btnIcloudBulkDelete) dom.btnIcloudBulkDelete.disabled = !hasSelection;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setIcloudLoadingState(loading, summary = '') {
|
||||||
|
if (dom.btnIcloudRefresh) dom.btnIcloudRefresh.disabled = loading;
|
||||||
|
if (dom.btnIcloudDeleteUsed) dom.btnIcloudDeleteUsed.disabled = loading;
|
||||||
|
if (dom.btnIcloudLoginDone) dom.btnIcloudLoginDone.disabled = loading;
|
||||||
|
if (dom.inputIcloudSearch) dom.inputIcloudSearch.disabled = loading;
|
||||||
|
if (dom.selectIcloudFilter) dom.selectIcloudFilter.disabled = loading;
|
||||||
|
if (dom.checkboxIcloudSelectAll) dom.checkboxIcloudSelectAll.disabled = loading || getFilteredIcloudAliases().length === 0;
|
||||||
|
if (dom.btnIcloudBulkUsed) dom.btnIcloudBulkUsed.disabled = loading || selectedEmails.size === 0;
|
||||||
|
if (dom.btnIcloudBulkUnused) dom.btnIcloudBulkUnused.disabled = loading || selectedEmails.size === 0;
|
||||||
|
if (dom.btnIcloudBulkPreserve) dom.btnIcloudBulkPreserve.disabled = loading || selectedEmails.size === 0;
|
||||||
|
if (dom.btnIcloudBulkUnpreserve) dom.btnIcloudBulkUnpreserve.disabled = loading || selectedEmails.size === 0;
|
||||||
|
if (dom.btnIcloudBulkDelete) dom.btnIcloudBulkDelete.disabled = loading || selectedEmails.size === 0;
|
||||||
|
if (summary && dom.icloudSummary) dom.icloudSummary.textContent = summary;
|
||||||
|
}
|
||||||
|
|
||||||
|
function showIcloudLoginHelp(payload = {}) {
|
||||||
|
if (!dom.icloudLoginHelp) return;
|
||||||
|
const loginUrl = String(payload.loginUrl || '').trim();
|
||||||
|
let host = 'icloud.com.cn / icloud.com';
|
||||||
|
if (loginUrl) {
|
||||||
|
try {
|
||||||
|
host = new URL(loginUrl).host;
|
||||||
|
} catch {
|
||||||
|
host = loginUrl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (dom.icloudLoginHelpTitle) dom.icloudLoginHelpTitle.textContent = '需要登录 iCloud';
|
||||||
|
if (dom.icloudLoginHelpText) dom.icloudLoginHelpText.textContent = `我已经为你打开 ${host}。请在那个页面完成登录,然后回到这里点击“我已登录”。`;
|
||||||
|
dom.icloudLoginHelp.style.display = 'flex';
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideIcloudLoginHelp() {
|
||||||
|
if (dom.icloudLoginHelp) {
|
||||||
|
dom.icloudLoginHelp.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderIcloudAliases(aliases = []) {
|
||||||
|
if (!dom.icloudList || !dom.icloudSummary) return;
|
||||||
|
|
||||||
|
renderedAliases = Array.isArray(aliases) ? aliases : [];
|
||||||
|
pruneIcloudSelection(renderedAliases);
|
||||||
|
dom.icloudList.innerHTML = '';
|
||||||
|
|
||||||
|
if (!aliases.length) {
|
||||||
|
selectedEmails.clear();
|
||||||
|
dom.icloudList.innerHTML = '<div class="icloud-empty">未找到 iCloud Hide My Email 别名。</div>';
|
||||||
|
dom.icloudSummary.textContent = '加载你的 iCloud Hide My Email 别名以便在这里管理。';
|
||||||
|
if (dom.btnIcloudDeleteUsed) dom.btnIcloudDeleteUsed.disabled = true;
|
||||||
|
updateIcloudBulkUI([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const usedCount = aliases.filter((alias) => alias.used).length;
|
||||||
|
const deletableUsedCount = aliases.filter((alias) => alias.used && !alias.preserved).length;
|
||||||
|
dom.icloudSummary.textContent = `已加载 ${aliases.length} 个别名,其中 ${usedCount} 个已标记为已用。`;
|
||||||
|
if (dom.btnIcloudDeleteUsed) dom.btnIcloudDeleteUsed.disabled = deletableUsedCount === 0;
|
||||||
|
|
||||||
|
const visibleAliases = getFilteredIcloudAliases(aliases);
|
||||||
|
if (!visibleAliases.length) {
|
||||||
|
dom.icloudList.innerHTML = '<div class="icloud-empty">没有匹配当前筛选条件的别名。</div>';
|
||||||
|
updateIcloudBulkUI([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const alias of visibleAliases) {
|
||||||
|
const item = document.createElement('div');
|
||||||
|
item.className = 'icloud-item';
|
||||||
|
item.innerHTML = `
|
||||||
|
<input class="icloud-item-check" type="checkbox" data-action="select" ${selectedEmails.has(alias.email) ? 'checked' : ''} />
|
||||||
|
<div class="icloud-item-main">
|
||||||
|
<div class="icloud-item-email">${helpers.escapeHtml(alias.email)}</div>
|
||||||
|
<div class="icloud-item-meta">
|
||||||
|
${alias.used ? '<span class="icloud-tag used">已用</span>' : ''}
|
||||||
|
${!alias.used && alias.active ? '<span class="icloud-tag active">可用</span>' : ''}
|
||||||
|
${alias.preserved ? '<span class="icloud-tag">保留</span>' : ''}
|
||||||
|
${alias.label ? `<span class="icloud-tag">${helpers.escapeHtml(alias.label)}</span>` : ''}
|
||||||
|
${alias.note ? `<span class="icloud-tag">${helpers.escapeHtml(alias.note)}</span>` : ''}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="icloud-item-actions">
|
||||||
|
<button class="btn btn-outline btn-xs" type="button" data-action="toggle-used">${helpers.escapeHtml(alias.used ? '标记未用' : '标记已用')}</button>
|
||||||
|
<button class="btn btn-outline btn-xs" type="button" data-action="toggle-preserved">${helpers.escapeHtml(alias.preserved ? '取消保留' : '保留')}</button>
|
||||||
|
<button class="btn btn-outline btn-xs" type="button" data-action="delete">删除</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
item.querySelector('[data-action="select"]').addEventListener('change', (event) => {
|
||||||
|
if (event.target.checked) {
|
||||||
|
selectedEmails.add(alias.email);
|
||||||
|
} else {
|
||||||
|
selectedEmails.delete(alias.email);
|
||||||
|
}
|
||||||
|
updateIcloudBulkUI(visibleAliases);
|
||||||
|
});
|
||||||
|
item.querySelector('[data-action="toggle-used"]').addEventListener('click', async () => {
|
||||||
|
await setSingleIcloudAliasUsedState(alias, !alias.used);
|
||||||
|
});
|
||||||
|
item.querySelector('[data-action="toggle-preserved"]').addEventListener('click', async () => {
|
||||||
|
await setSingleIcloudAliasPreservedState(alias, !alias.preserved);
|
||||||
|
});
|
||||||
|
item.querySelector('[data-action="delete"]').addEventListener('click', async () => {
|
||||||
|
await deleteSingleIcloudAlias(alias);
|
||||||
|
});
|
||||||
|
dom.icloudList.appendChild(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateIcloudBulkUI(visibleAliases);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshIcloudAliases(options = {}) {
|
||||||
|
const { silent = false } = options;
|
||||||
|
if (!dom.icloudSection || dom.icloudSection.style.display === 'none') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!silent) setIcloudLoadingState(true, '正在加载 iCloud 别名...');
|
||||||
|
try {
|
||||||
|
const response = await runtime.sendMessage({
|
||||||
|
type: 'LIST_ICLOUD_ALIASES',
|
||||||
|
source: 'sidepanel',
|
||||||
|
payload: {},
|
||||||
|
});
|
||||||
|
if (response?.error) throw new Error(response.error);
|
||||||
|
hideIcloudLoginHelp();
|
||||||
|
renderIcloudAliases(response?.aliases || []);
|
||||||
|
} catch (err) {
|
||||||
|
selectedEmails.clear();
|
||||||
|
if (dom.icloudList) {
|
||||||
|
dom.icloudList.innerHTML = '<div class="icloud-empty">无法加载 iCloud 别名。</div>';
|
||||||
|
}
|
||||||
|
if (dom.icloudSummary) {
|
||||||
|
dom.icloudSummary.textContent = err.message;
|
||||||
|
}
|
||||||
|
updateIcloudBulkUI([]);
|
||||||
|
if (!silent) helpers.showToast(`iCloud 别名加载失败:${err.message}`, 'error');
|
||||||
|
} finally {
|
||||||
|
setIcloudLoadingState(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function queueIcloudAliasRefresh() {
|
||||||
|
if (refreshQueued) return;
|
||||||
|
refreshQueued = true;
|
||||||
|
setTimeout(async () => {
|
||||||
|
refreshQueued = false;
|
||||||
|
await refreshIcloudAliases({ silent: true });
|
||||||
|
}, 150);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteSingleIcloudAlias(alias) {
|
||||||
|
const confirmed = await helpers.openConfirmModal({
|
||||||
|
title: '删除 iCloud 别名',
|
||||||
|
message: `确认删除 ${alias.email} 吗?此操作不可撤销。`,
|
||||||
|
confirmLabel: '确认删除',
|
||||||
|
confirmVariant: 'btn-danger',
|
||||||
|
});
|
||||||
|
if (!confirmed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIcloudLoadingState(true, `正在删除 ${alias.email} ...`);
|
||||||
|
try {
|
||||||
|
const response = await runtime.sendMessage({
|
||||||
|
type: 'DELETE_ICLOUD_ALIAS',
|
||||||
|
source: 'sidepanel',
|
||||||
|
payload: { email: alias.email, anonymousId: alias.anonymousId },
|
||||||
|
});
|
||||||
|
if (response?.error) throw new Error(response.error);
|
||||||
|
helpers.showToast(`已删除 ${alias.email}`, 'success', 2200);
|
||||||
|
await refreshIcloudAliases({ silent: true });
|
||||||
|
} catch (err) {
|
||||||
|
if (dom.icloudSummary) dom.icloudSummary.textContent = err.message;
|
||||||
|
helpers.showToast(`删除 iCloud 别名失败:${err.message}`, 'error');
|
||||||
|
} finally {
|
||||||
|
setIcloudLoadingState(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setSingleIcloudAliasUsedState(alias, used) {
|
||||||
|
setIcloudLoadingState(true, `正在更新 ${alias.email} 的使用状态...`);
|
||||||
|
try {
|
||||||
|
const response = await runtime.sendMessage({
|
||||||
|
type: 'SET_ICLOUD_ALIAS_USED_STATE',
|
||||||
|
source: 'sidepanel',
|
||||||
|
payload: { email: alias.email, used },
|
||||||
|
});
|
||||||
|
if (response?.error) throw new Error(response.error);
|
||||||
|
helpers.showToast(`${alias.email} 已${used ? '标记为已用' : '恢复为未用'}`, 'success', 2200);
|
||||||
|
await refreshIcloudAliases({ silent: true });
|
||||||
|
} catch (err) {
|
||||||
|
if (dom.icloudSummary) dom.icloudSummary.textContent = err.message;
|
||||||
|
helpers.showToast(`更新 iCloud 使用状态失败:${err.message}`, 'error');
|
||||||
|
} finally {
|
||||||
|
setIcloudLoadingState(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setSingleIcloudAliasPreservedState(alias, preserved) {
|
||||||
|
setIcloudLoadingState(true, `正在更新 ${alias.email} 的保留状态...`);
|
||||||
|
try {
|
||||||
|
const response = await runtime.sendMessage({
|
||||||
|
type: 'SET_ICLOUD_ALIAS_PRESERVED_STATE',
|
||||||
|
source: 'sidepanel',
|
||||||
|
payload: { email: alias.email, preserved },
|
||||||
|
});
|
||||||
|
if (response?.error) throw new Error(response.error);
|
||||||
|
helpers.showToast(`${alias.email} 已${preserved ? '设为保留' : '取消保留'}`, 'success', 2200);
|
||||||
|
await refreshIcloudAliases({ silent: true });
|
||||||
|
} catch (err) {
|
||||||
|
if (dom.icloudSummary) dom.icloudSummary.textContent = err.message;
|
||||||
|
helpers.showToast(`更新 iCloud 保留状态失败:${err.message}`, 'error');
|
||||||
|
} finally {
|
||||||
|
setIcloudLoadingState(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runBulkIcloudAction(action) {
|
||||||
|
const selectedAliases = renderedAliases.filter((alias) => selectedEmails.has(alias.email));
|
||||||
|
if (!selectedAliases.length) {
|
||||||
|
updateIcloudBulkUI();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === 'delete') {
|
||||||
|
const confirmed = await helpers.openConfirmModal({
|
||||||
|
title: '批量删除 iCloud 别名',
|
||||||
|
message: `确认删除选中的 ${selectedAliases.length} 个 iCloud 别名吗?此操作不可撤销。`,
|
||||||
|
confirmLabel: '确认删除',
|
||||||
|
confirmVariant: 'btn-danger',
|
||||||
|
});
|
||||||
|
if (!confirmed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const actionLabelMap = {
|
||||||
|
used: '标记已用',
|
||||||
|
unused: '标记未用',
|
||||||
|
preserve: '保留',
|
||||||
|
unpreserve: '取消保留',
|
||||||
|
delete: '删除',
|
||||||
|
};
|
||||||
|
setIcloudLoadingState(true, `正在批量${actionLabelMap[action] || '处理'} iCloud 别名...`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (const alias of selectedAliases) {
|
||||||
|
let response = null;
|
||||||
|
if (action === 'used' || action === 'unused') {
|
||||||
|
response = await runtime.sendMessage({
|
||||||
|
type: 'SET_ICLOUD_ALIAS_USED_STATE',
|
||||||
|
source: 'sidepanel',
|
||||||
|
payload: { email: alias.email, used: action === 'used' },
|
||||||
|
});
|
||||||
|
} else if (action === 'preserve' || action === 'unpreserve') {
|
||||||
|
response = await runtime.sendMessage({
|
||||||
|
type: 'SET_ICLOUD_ALIAS_PRESERVED_STATE',
|
||||||
|
source: 'sidepanel',
|
||||||
|
payload: { email: alias.email, preserved: action === 'preserve' },
|
||||||
|
});
|
||||||
|
} else if (action === 'delete') {
|
||||||
|
response = await runtime.sendMessage({
|
||||||
|
type: 'DELETE_ICLOUD_ALIAS',
|
||||||
|
source: 'sidepanel',
|
||||||
|
payload: { email: alias.email, anonymousId: alias.anonymousId },
|
||||||
|
});
|
||||||
|
selectedEmails.delete(alias.email);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response?.error) {
|
||||||
|
throw new Error(response.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
helpers.showToast(`已批量${actionLabelMap[action] || '处理'} ${selectedAliases.length} 个 iCloud 别名`, 'success', 2400);
|
||||||
|
await refreshIcloudAliases({ silent: true });
|
||||||
|
} catch (err) {
|
||||||
|
if (dom.icloudSummary) dom.icloudSummary.textContent = err.message;
|
||||||
|
helpers.showToast(`批量处理 iCloud 别名失败:${err.message}`, 'error');
|
||||||
|
} finally {
|
||||||
|
setIcloudLoadingState(false);
|
||||||
|
updateIcloudBulkUI();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteUsedIcloudAliases() {
|
||||||
|
const confirmed = await helpers.openConfirmModal({
|
||||||
|
title: '删除已用 iCloud 别名',
|
||||||
|
message: '确认删除所有未保留的已用 iCloud 别名吗?此操作不可撤销。',
|
||||||
|
confirmLabel: '确认删除',
|
||||||
|
confirmVariant: 'btn-danger',
|
||||||
|
});
|
||||||
|
if (!confirmed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIcloudLoadingState(true, '正在删除已用 iCloud 别名...');
|
||||||
|
try {
|
||||||
|
const response = await runtime.sendMessage({
|
||||||
|
type: 'DELETE_USED_ICLOUD_ALIASES',
|
||||||
|
source: 'sidepanel',
|
||||||
|
payload: {},
|
||||||
|
});
|
||||||
|
if (response?.error) throw new Error(response.error);
|
||||||
|
const deleted = response?.deleted || [];
|
||||||
|
const skipped = response?.skipped || [];
|
||||||
|
helpers.showToast(`已删除 ${deleted.length} 个已用别名,跳过 ${skipped.length} 个`, skipped.length ? 'warn' : 'success', 2800);
|
||||||
|
await refreshIcloudAliases({ silent: true });
|
||||||
|
} catch (err) {
|
||||||
|
if (dom.icloudSummary) dom.icloudSummary.textContent = err.message;
|
||||||
|
helpers.showToast(`删除已用 iCloud 别名失败:${err.message}`, 'error');
|
||||||
|
} finally {
|
||||||
|
setIcloudLoadingState(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleLoginDone() {
|
||||||
|
if (dom.btnIcloudLoginDone) {
|
||||||
|
dom.btnIcloudLoginDone.disabled = true;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const response = await runtime.sendMessage({
|
||||||
|
type: 'CHECK_ICLOUD_SESSION',
|
||||||
|
source: 'sidepanel',
|
||||||
|
payload: {},
|
||||||
|
});
|
||||||
|
if (response?.error) {
|
||||||
|
throw new Error(response.error);
|
||||||
|
}
|
||||||
|
hideIcloudLoginHelp();
|
||||||
|
helpers.showToast('iCloud 会话已恢复,别名列表已刷新。', 'success', 2600);
|
||||||
|
await refreshIcloudAliases({ silent: true });
|
||||||
|
} catch (err) {
|
||||||
|
helpers.showToast(`看起来还没有登录完成:${err.message}`, 'warn', 4200);
|
||||||
|
} finally {
|
||||||
|
if (dom.btnIcloudLoginDone) {
|
||||||
|
dom.btnIcloudLoginDone.disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function reset() {
|
||||||
|
selectedEmails.clear();
|
||||||
|
renderedAliases = [];
|
||||||
|
searchTerm = '';
|
||||||
|
filterMode = 'all';
|
||||||
|
refreshQueued = false;
|
||||||
|
if (dom.inputIcloudSearch) dom.inputIcloudSearch.value = '';
|
||||||
|
if (dom.selectIcloudFilter) dom.selectIcloudFilter.value = 'all';
|
||||||
|
if (dom.icloudList) dom.icloudList.innerHTML = '';
|
||||||
|
if (dom.icloudSummary) dom.icloudSummary.textContent = '加载你的 iCloud Hide My Email 别名以便在这里管理。';
|
||||||
|
updateIcloudBulkUI([]);
|
||||||
|
hideIcloudLoginHelp();
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasDeletableUsedAliases() {
|
||||||
|
return renderedAliases.some((alias) => alias.used && !alias.preserved);
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindIcloudEvents() {
|
||||||
|
dom.btnIcloudRefresh?.addEventListener('click', async () => {
|
||||||
|
await refreshIcloudAliases();
|
||||||
|
});
|
||||||
|
|
||||||
|
dom.btnIcloudDeleteUsed?.addEventListener('click', async () => {
|
||||||
|
await deleteUsedIcloudAliases();
|
||||||
|
});
|
||||||
|
|
||||||
|
dom.inputIcloudSearch?.addEventListener('input', () => {
|
||||||
|
searchTerm = dom.inputIcloudSearch.value || '';
|
||||||
|
renderIcloudAliases(renderedAliases);
|
||||||
|
});
|
||||||
|
|
||||||
|
dom.selectIcloudFilter?.addEventListener('change', () => {
|
||||||
|
filterMode = dom.selectIcloudFilter.value || 'all';
|
||||||
|
renderIcloudAliases(renderedAliases);
|
||||||
|
});
|
||||||
|
|
||||||
|
dom.checkboxIcloudSelectAll?.addEventListener('change', () => {
|
||||||
|
const visibleAliases = getFilteredIcloudAliases();
|
||||||
|
if (dom.checkboxIcloudSelectAll.checked) {
|
||||||
|
visibleAliases.forEach((alias) => selectedEmails.add(alias.email));
|
||||||
|
} else {
|
||||||
|
visibleAliases.forEach((alias) => selectedEmails.delete(alias.email));
|
||||||
|
}
|
||||||
|
renderIcloudAliases(renderedAliases);
|
||||||
|
});
|
||||||
|
|
||||||
|
dom.btnIcloudBulkUsed?.addEventListener('click', async () => {
|
||||||
|
await runBulkIcloudAction('used');
|
||||||
|
});
|
||||||
|
|
||||||
|
dom.btnIcloudBulkUnused?.addEventListener('click', async () => {
|
||||||
|
await runBulkIcloudAction('unused');
|
||||||
|
});
|
||||||
|
|
||||||
|
dom.btnIcloudBulkPreserve?.addEventListener('click', async () => {
|
||||||
|
await runBulkIcloudAction('preserve');
|
||||||
|
});
|
||||||
|
|
||||||
|
dom.btnIcloudBulkUnpreserve?.addEventListener('click', async () => {
|
||||||
|
await runBulkIcloudAction('unpreserve');
|
||||||
|
});
|
||||||
|
|
||||||
|
dom.btnIcloudBulkDelete?.addEventListener('click', async () => {
|
||||||
|
await runBulkIcloudAction('delete');
|
||||||
|
});
|
||||||
|
|
||||||
|
dom.btnIcloudLoginDone?.addEventListener('click', handleLoginDone);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
bindIcloudEvents,
|
||||||
|
hideIcloudLoginHelp,
|
||||||
|
hasDeletableUsedAliases,
|
||||||
|
queueIcloudAliasRefresh,
|
||||||
|
refreshIcloudAliases,
|
||||||
|
renderIcloudAliases,
|
||||||
|
reset,
|
||||||
|
showIcloudLoginHelp,
|
||||||
|
updateIcloudBulkUI,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
globalScope.SidepanelIcloudManager = {
|
||||||
|
createIcloudManager,
|
||||||
|
};
|
||||||
|
})(window);
|
||||||
@@ -0,0 +1,467 @@
|
|||||||
|
(function attachSidepanelLuckmailManager(globalScope) {
|
||||||
|
function createLuckmailManager(context = {}) {
|
||||||
|
const {
|
||||||
|
dom,
|
||||||
|
helpers,
|
||||||
|
runtime,
|
||||||
|
constants = {},
|
||||||
|
} = context;
|
||||||
|
|
||||||
|
const copyIcon = constants.copyIcon || '';
|
||||||
|
|
||||||
|
let renderedPurchases = [];
|
||||||
|
let selectedPurchaseIds = new Set();
|
||||||
|
let searchTerm = '';
|
||||||
|
let filterMode = 'all';
|
||||||
|
let refreshQueued = false;
|
||||||
|
|
||||||
|
function normalizeLuckmailSearchText(value) {
|
||||||
|
return String(value || '').trim().toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFilteredLuckmailPurchases(purchases = renderedPurchases) {
|
||||||
|
const normalizedSearchTerm = normalizeLuckmailSearchText(searchTerm);
|
||||||
|
return (Array.isArray(purchases) ? purchases : []).filter((purchase) => {
|
||||||
|
const matchesFilter = (() => {
|
||||||
|
switch (filterMode) {
|
||||||
|
case 'reusable': return Boolean(purchase.reusable);
|
||||||
|
case 'used': return Boolean(purchase.used);
|
||||||
|
case 'unused': return !purchase.used;
|
||||||
|
case 'preserved': return Boolean(purchase.preserved);
|
||||||
|
case 'disabled': return Boolean(purchase.disabled);
|
||||||
|
default: return true;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
if (!matchesFilter) return false;
|
||||||
|
if (!normalizedSearchTerm) return true;
|
||||||
|
|
||||||
|
const haystack = [
|
||||||
|
purchase.email_address,
|
||||||
|
purchase.project_name,
|
||||||
|
purchase.tag_name,
|
||||||
|
purchase.used ? '已用 used' : '未用 unused',
|
||||||
|
purchase.preserved ? '保留 preserved' : '',
|
||||||
|
purchase.disabled ? '已禁用 disabled' : '',
|
||||||
|
purchase.reusable ? '可复用 reusable' : '',
|
||||||
|
].join(' ').toLowerCase();
|
||||||
|
|
||||||
|
return haystack.includes(normalizedSearchTerm);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function pruneLuckmailSelection(purchases = renderedPurchases) {
|
||||||
|
const existingIds = new Set((Array.isArray(purchases) ? purchases : []).map((purchase) => String(purchase.id)));
|
||||||
|
selectedPurchaseIds = new Set([...selectedPurchaseIds].filter((id) => existingIds.has(id)));
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateLuckmailBulkUI(visiblePurchases = getFilteredLuckmailPurchases()) {
|
||||||
|
if (!dom.checkboxLuckmailSelectAll || !dom.luckmailSelectionSummary) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const visibleIds = visiblePurchases.map((purchase) => String(purchase.id));
|
||||||
|
const selectedVisibleCount = visibleIds.filter((id) => selectedPurchaseIds.has(id)).length;
|
||||||
|
const hasVisible = visibleIds.length > 0;
|
||||||
|
const hasSelection = selectedPurchaseIds.size > 0;
|
||||||
|
|
||||||
|
dom.checkboxLuckmailSelectAll.checked = hasVisible && selectedVisibleCount === visibleIds.length;
|
||||||
|
dom.checkboxLuckmailSelectAll.indeterminate = selectedVisibleCount > 0 && selectedVisibleCount < visibleIds.length;
|
||||||
|
dom.checkboxLuckmailSelectAll.disabled = !hasVisible;
|
||||||
|
dom.luckmailSelectionSummary.textContent = `已选 ${selectedPurchaseIds.size} 个(当前显示 ${visibleIds.length} 个)`;
|
||||||
|
|
||||||
|
if (dom.btnLuckmailBulkUsed) dom.btnLuckmailBulkUsed.disabled = !hasSelection;
|
||||||
|
if (dom.btnLuckmailBulkUnused) dom.btnLuckmailBulkUnused.disabled = !hasSelection;
|
||||||
|
if (dom.btnLuckmailBulkPreserve) dom.btnLuckmailBulkPreserve.disabled = !hasSelection;
|
||||||
|
if (dom.btnLuckmailBulkUnpreserve) dom.btnLuckmailBulkUnpreserve.disabled = !hasSelection;
|
||||||
|
if (dom.btnLuckmailBulkDisable) dom.btnLuckmailBulkDisable.disabled = !hasSelection;
|
||||||
|
if (dom.btnLuckmailBulkEnable) dom.btnLuckmailBulkEnable.disabled = !hasSelection;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setLuckmailLoadingState(loading, summary = '') {
|
||||||
|
if (dom.btnLuckmailRefresh) dom.btnLuckmailRefresh.disabled = loading;
|
||||||
|
if (dom.btnLuckmailDisableUsed) dom.btnLuckmailDisableUsed.disabled = loading;
|
||||||
|
if (dom.inputLuckmailSearch) dom.inputLuckmailSearch.disabled = loading;
|
||||||
|
if (dom.selectLuckmailFilter) dom.selectLuckmailFilter.disabled = loading;
|
||||||
|
if (dom.checkboxLuckmailSelectAll) dom.checkboxLuckmailSelectAll.disabled = loading || getFilteredLuckmailPurchases().length === 0;
|
||||||
|
if (dom.btnLuckmailBulkUsed) dom.btnLuckmailBulkUsed.disabled = loading || selectedPurchaseIds.size === 0;
|
||||||
|
if (dom.btnLuckmailBulkUnused) dom.btnLuckmailBulkUnused.disabled = loading || selectedPurchaseIds.size === 0;
|
||||||
|
if (dom.btnLuckmailBulkPreserve) dom.btnLuckmailBulkPreserve.disabled = loading || selectedPurchaseIds.size === 0;
|
||||||
|
if (dom.btnLuckmailBulkUnpreserve) dom.btnLuckmailBulkUnpreserve.disabled = loading || selectedPurchaseIds.size === 0;
|
||||||
|
if (dom.btnLuckmailBulkDisable) dom.btnLuckmailBulkDisable.disabled = loading || selectedPurchaseIds.size === 0;
|
||||||
|
if (dom.btnLuckmailBulkEnable) dom.btnLuckmailBulkEnable.disabled = loading || selectedPurchaseIds.size === 0;
|
||||||
|
if (summary && dom.luckmailSummary) {
|
||||||
|
dom.luckmailSummary.textContent = summary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderLuckmailPurchases(purchases = renderedPurchases) {
|
||||||
|
if (!dom.luckmailList || !dom.luckmailSummary) return;
|
||||||
|
|
||||||
|
renderedPurchases = Array.isArray(purchases) ? purchases : [];
|
||||||
|
pruneLuckmailSelection(renderedPurchases);
|
||||||
|
dom.luckmailList.innerHTML = '';
|
||||||
|
|
||||||
|
if (!renderedPurchases.length) {
|
||||||
|
selectedPurchaseIds.clear();
|
||||||
|
dom.luckmailList.innerHTML = '<div class="luckmail-empty">未找到 openai 项目的 LuckMail 邮箱。</div>';
|
||||||
|
dom.luckmailSummary.textContent = '加载已购邮箱后可在这里管理 openai 项目的 LuckMail 邮箱。';
|
||||||
|
if (dom.btnLuckmailDisableUsed) dom.btnLuckmailDisableUsed.disabled = true;
|
||||||
|
updateLuckmailBulkUI([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const usedCount = renderedPurchases.filter((purchase) => purchase.used).length;
|
||||||
|
const reusableCount = renderedPurchases.filter((purchase) => purchase.reusable).length;
|
||||||
|
const disableUsedCount = renderedPurchases.filter((purchase) => purchase.used && !purchase.preserved && !purchase.disabled).length;
|
||||||
|
dom.luckmailSummary.textContent = `已加载 ${renderedPurchases.length} 个 openai 邮箱,其中 ${reusableCount} 个可复用,${usedCount} 个已本地标记为已用。`;
|
||||||
|
if (dom.btnLuckmailDisableUsed) {
|
||||||
|
dom.btnLuckmailDisableUsed.textContent = `禁用已用${disableUsedCount > 0 ? `(${disableUsedCount})` : ''}`;
|
||||||
|
dom.btnLuckmailDisableUsed.disabled = disableUsedCount === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const visiblePurchases = getFilteredLuckmailPurchases(renderedPurchases);
|
||||||
|
if (!visiblePurchases.length) {
|
||||||
|
dom.luckmailList.innerHTML = '<div class="luckmail-empty">没有匹配当前筛选条件的 LuckMail 邮箱。</div>';
|
||||||
|
updateLuckmailBulkUI([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const purchase of visiblePurchases) {
|
||||||
|
const purchaseId = String(purchase.id);
|
||||||
|
const item = document.createElement('div');
|
||||||
|
item.className = `luckmail-item${purchase.current ? ' is-current' : ''}`;
|
||||||
|
item.innerHTML = `
|
||||||
|
<input class="luckmail-item-check" type="checkbox" data-action="select" ${selectedPurchaseIds.has(purchaseId) ? 'checked' : ''} />
|
||||||
|
<div class="luckmail-item-main">
|
||||||
|
<div class="luckmail-item-email-row">
|
||||||
|
<div class="luckmail-item-email">${helpers.escapeHtml(purchase.email_address || '(未知邮箱)')}</div>
|
||||||
|
<button
|
||||||
|
class="hotmail-copy-btn"
|
||||||
|
type="button"
|
||||||
|
data-action="copy-email"
|
||||||
|
title="复制邮箱"
|
||||||
|
aria-label="复制邮箱 ${helpers.escapeHtml(purchase.email_address || '')}"
|
||||||
|
>${copyIcon}</button>
|
||||||
|
</div>
|
||||||
|
<div class="luckmail-item-meta">
|
||||||
|
<span class="luckmail-tag">${helpers.escapeHtml(helpers.normalizeLuckmailProjectName(purchase.project_name) || 'openai')}</span>
|
||||||
|
${purchase.reusable ? '<span class="luckmail-tag active">可复用</span>' : ''}
|
||||||
|
${purchase.current ? '<span class="luckmail-tag current">当前</span>' : ''}
|
||||||
|
${purchase.used ? '<span class="luckmail-tag used">已用</span>' : ''}
|
||||||
|
${purchase.preserved ? '<span class="luckmail-tag">保留</span>' : ''}
|
||||||
|
${purchase.disabled ? '<span class="luckmail-tag disabled">已禁用</span>' : ''}
|
||||||
|
${purchase.tag_name && normalizeLuckmailSearchText(purchase.tag_name) !== normalizeLuckmailSearchText(helpers.getLuckmailPreserveTagName())
|
||||||
|
? `<span class="luckmail-tag">${helpers.escapeHtml(purchase.tag_name)}</span>`
|
||||||
|
: ''}
|
||||||
|
</div>
|
||||||
|
<div class="luckmail-item-details">
|
||||||
|
<span>ID:${helpers.escapeHtml(String(purchase.id || ''))}</span>
|
||||||
|
<span>保修至:${helpers.escapeHtml(helpers.formatLuckmailDateTime(purchase.warranty_until))}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="luckmail-item-actions">
|
||||||
|
<button class="btn btn-outline btn-xs" type="button" data-action="use">使用此邮箱</button>
|
||||||
|
<button class="btn btn-outline btn-xs" type="button" data-action="toggle-used">${helpers.escapeHtml(purchase.used ? '标记未用' : '标记已用')}</button>
|
||||||
|
<button class="btn btn-outline btn-xs" type="button" data-action="toggle-preserved">${helpers.escapeHtml(purchase.preserved ? '取消保留' : '保留')}</button>
|
||||||
|
<button class="btn btn-outline btn-xs" type="button" data-action="toggle-disabled">${helpers.escapeHtml(purchase.disabled ? '启用' : '禁用')}</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
item.querySelector('[data-action="select"]').addEventListener('change', (event) => {
|
||||||
|
if (event.target.checked) {
|
||||||
|
selectedPurchaseIds.add(purchaseId);
|
||||||
|
} else {
|
||||||
|
selectedPurchaseIds.delete(purchaseId);
|
||||||
|
}
|
||||||
|
updateLuckmailBulkUI(visiblePurchases);
|
||||||
|
});
|
||||||
|
item.querySelector('[data-action="copy-email"]').addEventListener('click', async () => {
|
||||||
|
await helpers.copyTextToClipboard(purchase.email_address || '');
|
||||||
|
helpers.showToast('邮箱已复制', 'success', 1600);
|
||||||
|
});
|
||||||
|
item.querySelector('[data-action="use"]').addEventListener('click', async () => {
|
||||||
|
await selectSingleLuckmailPurchase(purchase);
|
||||||
|
});
|
||||||
|
item.querySelector('[data-action="toggle-used"]').addEventListener('click', async () => {
|
||||||
|
await setSingleLuckmailPurchaseUsedState(purchase, !purchase.used);
|
||||||
|
});
|
||||||
|
item.querySelector('[data-action="toggle-preserved"]').addEventListener('click', async () => {
|
||||||
|
await setSingleLuckmailPurchasePreservedState(purchase, !purchase.preserved);
|
||||||
|
});
|
||||||
|
item.querySelector('[data-action="toggle-disabled"]').addEventListener('click', async () => {
|
||||||
|
await setSingleLuckmailPurchaseDisabledState(purchase, !purchase.disabled);
|
||||||
|
});
|
||||||
|
dom.luckmailList.appendChild(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateLuckmailBulkUI(visiblePurchases);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshLuckmailPurchases(options = {}) {
|
||||||
|
const { silent = false } = options;
|
||||||
|
if (!dom.luckmailSection || dom.luckmailSection.style.display === 'none') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!silent) setLuckmailLoadingState(true, '正在加载 LuckMail openai 邮箱...');
|
||||||
|
try {
|
||||||
|
const response = await runtime.sendMessage({
|
||||||
|
type: 'LIST_LUCKMAIL_PURCHASES',
|
||||||
|
source: 'sidepanel',
|
||||||
|
payload: {},
|
||||||
|
});
|
||||||
|
if (response?.error) throw new Error(response.error);
|
||||||
|
renderLuckmailPurchases(response?.purchases || []);
|
||||||
|
} catch (err) {
|
||||||
|
selectedPurchaseIds.clear();
|
||||||
|
if (dom.luckmailList) {
|
||||||
|
dom.luckmailList.innerHTML = '<div class="luckmail-empty">无法加载 LuckMail 邮箱列表。</div>';
|
||||||
|
}
|
||||||
|
if (dom.luckmailSummary) {
|
||||||
|
dom.luckmailSummary.textContent = err.message;
|
||||||
|
}
|
||||||
|
updateLuckmailBulkUI([]);
|
||||||
|
if (!silent) {
|
||||||
|
helpers.showToast(`LuckMail 邮箱列表加载失败:${err.message}`, 'error');
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setLuckmailLoadingState(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function queueLuckmailPurchaseRefresh() {
|
||||||
|
if (refreshQueued) return;
|
||||||
|
refreshQueued = true;
|
||||||
|
setTimeout(async () => {
|
||||||
|
refreshQueued = false;
|
||||||
|
await refreshLuckmailPurchases({ silent: true });
|
||||||
|
}, 150);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function selectSingleLuckmailPurchase(purchase) {
|
||||||
|
setLuckmailLoadingState(true, `正在切换到 ${purchase.email_address} ...`);
|
||||||
|
try {
|
||||||
|
const response = await runtime.sendMessage({
|
||||||
|
type: 'SELECT_LUCKMAIL_PURCHASE',
|
||||||
|
source: 'sidepanel',
|
||||||
|
payload: { purchaseId: purchase.id },
|
||||||
|
});
|
||||||
|
if (response?.error) throw new Error(response.error);
|
||||||
|
dom.inputEmail.value = response?.purchase?.email_address || purchase.email_address || '';
|
||||||
|
helpers.showToast(`已切换当前 LuckMail 邮箱为 ${purchase.email_address}`, 'success', 2200);
|
||||||
|
await refreshLuckmailPurchases({ silent: true });
|
||||||
|
} catch (err) {
|
||||||
|
if (dom.luckmailSummary) dom.luckmailSummary.textContent = err.message;
|
||||||
|
helpers.showToast(`切换 LuckMail 邮箱失败:${err.message}`, 'error');
|
||||||
|
} finally {
|
||||||
|
setLuckmailLoadingState(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setSingleLuckmailPurchaseUsedState(purchase, used) {
|
||||||
|
setLuckmailLoadingState(true, `正在更新 ${purchase.email_address} 的已用状态...`);
|
||||||
|
try {
|
||||||
|
const response = await runtime.sendMessage({
|
||||||
|
type: 'SET_LUCKMAIL_PURCHASE_USED_STATE',
|
||||||
|
source: 'sidepanel',
|
||||||
|
payload: { purchaseId: purchase.id, used },
|
||||||
|
});
|
||||||
|
if (response?.error) throw new Error(response.error);
|
||||||
|
helpers.showToast(`${purchase.email_address} 已${used ? '标记为已用' : '恢复为未用'}`, 'success', 2200);
|
||||||
|
await refreshLuckmailPurchases({ silent: true });
|
||||||
|
} catch (err) {
|
||||||
|
if (dom.luckmailSummary) dom.luckmailSummary.textContent = err.message;
|
||||||
|
helpers.showToast(`更新 LuckMail 已用状态失败:${err.message}`, 'error');
|
||||||
|
} finally {
|
||||||
|
setLuckmailLoadingState(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setSingleLuckmailPurchasePreservedState(purchase, preserved) {
|
||||||
|
setLuckmailLoadingState(true, `正在更新 ${purchase.email_address} 的保留状态...`);
|
||||||
|
try {
|
||||||
|
const response = await runtime.sendMessage({
|
||||||
|
type: 'SET_LUCKMAIL_PURCHASE_PRESERVED_STATE',
|
||||||
|
source: 'sidepanel',
|
||||||
|
payload: { purchaseId: purchase.id, preserved },
|
||||||
|
});
|
||||||
|
if (response?.error) throw new Error(response.error);
|
||||||
|
helpers.showToast(`${purchase.email_address} 已${preserved ? '设为保留' : '取消保留'}`, 'success', 2200);
|
||||||
|
await refreshLuckmailPurchases({ silent: true });
|
||||||
|
} catch (err) {
|
||||||
|
if (dom.luckmailSummary) dom.luckmailSummary.textContent = err.message;
|
||||||
|
helpers.showToast(`更新 LuckMail 保留状态失败:${err.message}`, 'error');
|
||||||
|
} finally {
|
||||||
|
setLuckmailLoadingState(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setSingleLuckmailPurchaseDisabledState(purchase, disabled) {
|
||||||
|
setLuckmailLoadingState(true, `正在${disabled ? '禁用' : '启用'} ${purchase.email_address} ...`);
|
||||||
|
try {
|
||||||
|
const response = await runtime.sendMessage({
|
||||||
|
type: 'SET_LUCKMAIL_PURCHASE_DISABLED_STATE',
|
||||||
|
source: 'sidepanel',
|
||||||
|
payload: { purchaseId: purchase.id, disabled },
|
||||||
|
});
|
||||||
|
if (response?.error) throw new Error(response.error);
|
||||||
|
helpers.showToast(`${purchase.email_address} 已${disabled ? '禁用' : '启用'}`, 'success', 2200);
|
||||||
|
await refreshLuckmailPurchases({ silent: true });
|
||||||
|
} catch (err) {
|
||||||
|
if (dom.luckmailSummary) dom.luckmailSummary.textContent = err.message;
|
||||||
|
helpers.showToast(`更新 LuckMail 禁用状态失败:${err.message}`, 'error');
|
||||||
|
} finally {
|
||||||
|
setLuckmailLoadingState(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runBulkLuckmailAction(action) {
|
||||||
|
const selectedIds = renderedPurchases
|
||||||
|
.filter((purchase) => selectedPurchaseIds.has(String(purchase.id)))
|
||||||
|
.map((purchase) => purchase.id);
|
||||||
|
if (!selectedIds.length) {
|
||||||
|
updateLuckmailBulkUI();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const actionLabelMap = {
|
||||||
|
used: '标记已用',
|
||||||
|
unused: '标记未用',
|
||||||
|
preserve: '保留',
|
||||||
|
unpreserve: '取消保留',
|
||||||
|
disable: '禁用',
|
||||||
|
enable: '启用',
|
||||||
|
};
|
||||||
|
|
||||||
|
setLuckmailLoadingState(true, `正在批量${actionLabelMap[action] || '处理'} LuckMail 邮箱...`);
|
||||||
|
try {
|
||||||
|
const response = await runtime.sendMessage({
|
||||||
|
type: 'BATCH_UPDATE_LUCKMAIL_PURCHASES',
|
||||||
|
source: 'sidepanel',
|
||||||
|
payload: { action, ids: selectedIds },
|
||||||
|
});
|
||||||
|
if (response?.error) throw new Error(response.error);
|
||||||
|
helpers.showToast(`已批量${actionLabelMap[action] || '处理'} ${selectedIds.length} 个 LuckMail 邮箱`, 'success', 2400);
|
||||||
|
await refreshLuckmailPurchases({ silent: true });
|
||||||
|
} catch (err) {
|
||||||
|
if (dom.luckmailSummary) dom.luckmailSummary.textContent = err.message;
|
||||||
|
helpers.showToast(`批量处理 LuckMail 邮箱失败:${err.message}`, 'error');
|
||||||
|
} finally {
|
||||||
|
setLuckmailLoadingState(false);
|
||||||
|
updateLuckmailBulkUI();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function disableUsedLuckmailPurchases() {
|
||||||
|
const confirmed = await helpers.openConfirmModal({
|
||||||
|
title: '禁用已用 LuckMail 邮箱',
|
||||||
|
message: '确认禁用所有本地已用且未保留的 openai LuckMail 邮箱吗?',
|
||||||
|
confirmLabel: '确认禁用',
|
||||||
|
confirmVariant: 'btn-danger',
|
||||||
|
});
|
||||||
|
if (!confirmed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLuckmailLoadingState(true, '正在禁用已用 LuckMail 邮箱...');
|
||||||
|
try {
|
||||||
|
const response = await runtime.sendMessage({
|
||||||
|
type: 'DISABLE_USED_LUCKMAIL_PURCHASES',
|
||||||
|
source: 'sidepanel',
|
||||||
|
payload: {},
|
||||||
|
});
|
||||||
|
if (response?.error) throw new Error(response.error);
|
||||||
|
const disabledCount = Array.isArray(response?.disabledIds) ? response.disabledIds.length : 0;
|
||||||
|
helpers.showToast(`已禁用 ${disabledCount} 个 LuckMail 邮箱`, disabledCount > 0 ? 'success' : 'info', 2400);
|
||||||
|
await refreshLuckmailPurchases({ silent: true });
|
||||||
|
} catch (err) {
|
||||||
|
if (dom.luckmailSummary) dom.luckmailSummary.textContent = err.message;
|
||||||
|
helpers.showToast(`禁用已用 LuckMail 邮箱失败:${err.message}`, 'error');
|
||||||
|
} finally {
|
||||||
|
setLuckmailLoadingState(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function reset() {
|
||||||
|
renderedPurchases = [];
|
||||||
|
selectedPurchaseIds.clear();
|
||||||
|
searchTerm = '';
|
||||||
|
filterMode = 'all';
|
||||||
|
refreshQueued = false;
|
||||||
|
if (dom.inputLuckmailSearch) dom.inputLuckmailSearch.value = '';
|
||||||
|
if (dom.selectLuckmailFilter) dom.selectLuckmailFilter.value = 'all';
|
||||||
|
if (dom.luckmailList) dom.luckmailList.innerHTML = '';
|
||||||
|
if (dom.luckmailSummary) dom.luckmailSummary.textContent = '加载已购邮箱后可在这里管理 openai 项目的 LuckMail 邮箱。';
|
||||||
|
if (dom.btnLuckmailDisableUsed) dom.btnLuckmailDisableUsed.disabled = true;
|
||||||
|
updateLuckmailBulkUI([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindLuckmailEvents() {
|
||||||
|
dom.inputLuckmailSearch?.addEventListener('input', (event) => {
|
||||||
|
searchTerm = event.target.value || '';
|
||||||
|
renderLuckmailPurchases(renderedPurchases);
|
||||||
|
});
|
||||||
|
|
||||||
|
dom.selectLuckmailFilter?.addEventListener('change', (event) => {
|
||||||
|
filterMode = String(event.target.value || 'all').trim() || 'all';
|
||||||
|
renderLuckmailPurchases(renderedPurchases);
|
||||||
|
});
|
||||||
|
|
||||||
|
dom.checkboxLuckmailSelectAll?.addEventListener('change', () => {
|
||||||
|
const visiblePurchases = getFilteredLuckmailPurchases();
|
||||||
|
if (dom.checkboxLuckmailSelectAll.checked) {
|
||||||
|
visiblePurchases.forEach((purchase) => selectedPurchaseIds.add(String(purchase.id)));
|
||||||
|
} else {
|
||||||
|
visiblePurchases.forEach((purchase) => selectedPurchaseIds.delete(String(purchase.id)));
|
||||||
|
}
|
||||||
|
renderLuckmailPurchases(renderedPurchases);
|
||||||
|
});
|
||||||
|
|
||||||
|
dom.btnLuckmailRefresh?.addEventListener('click', async () => {
|
||||||
|
await refreshLuckmailPurchases();
|
||||||
|
});
|
||||||
|
|
||||||
|
dom.btnLuckmailDisableUsed?.addEventListener('click', async () => {
|
||||||
|
await disableUsedLuckmailPurchases();
|
||||||
|
});
|
||||||
|
|
||||||
|
dom.btnLuckmailBulkUsed?.addEventListener('click', async () => {
|
||||||
|
await runBulkLuckmailAction('used');
|
||||||
|
});
|
||||||
|
|
||||||
|
dom.btnLuckmailBulkUnused?.addEventListener('click', async () => {
|
||||||
|
await runBulkLuckmailAction('unused');
|
||||||
|
});
|
||||||
|
|
||||||
|
dom.btnLuckmailBulkPreserve?.addEventListener('click', async () => {
|
||||||
|
await runBulkLuckmailAction('preserve');
|
||||||
|
});
|
||||||
|
|
||||||
|
dom.btnLuckmailBulkUnpreserve?.addEventListener('click', async () => {
|
||||||
|
await runBulkLuckmailAction('unpreserve');
|
||||||
|
});
|
||||||
|
|
||||||
|
dom.btnLuckmailBulkDisable?.addEventListener('click', async () => {
|
||||||
|
await runBulkLuckmailAction('disable');
|
||||||
|
});
|
||||||
|
|
||||||
|
dom.btnLuckmailBulkEnable?.addEventListener('click', async () => {
|
||||||
|
await runBulkLuckmailAction('enable');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
bindLuckmailEvents,
|
||||||
|
disableUsedLuckmailPurchases,
|
||||||
|
queueLuckmailPurchaseRefresh,
|
||||||
|
refreshLuckmailPurchases,
|
||||||
|
renderLuckmailPurchases,
|
||||||
|
reset,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
globalScope.SidepanelLuckmailManager = {
|
||||||
|
createLuckmailManager,
|
||||||
|
};
|
||||||
|
})(window);
|
||||||
@@ -611,6 +611,9 @@
|
|||||||
<script src="../hotmail-utils.js"></script>
|
<script src="../hotmail-utils.js"></script>
|
||||||
<script src="../luckmail-utils.js"></script>
|
<script src="../luckmail-utils.js"></script>
|
||||||
<script src="update-service.js"></script>
|
<script src="update-service.js"></script>
|
||||||
|
<script src="hotmail-manager.js"></script>
|
||||||
|
<script src="icloud-manager.js"></script>
|
||||||
|
<script src="luckmail-manager.js"></script>
|
||||||
<script src="sidepanel.js"></script>
|
<script src="sidepanel.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
|||||||
+163
-1381
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,17 @@
|
|||||||
|
const test = require('node:test');
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
|
||||||
|
test('background imports generated email helper module', () => {
|
||||||
|
const source = fs.readFileSync('background.js', 'utf8');
|
||||||
|
assert.match(source, /importScripts\([\s\S]*'background\/generated-email-helpers\.js'/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('generated email helper module exposes a factory', () => {
|
||||||
|
const source = fs.readFileSync('background/generated-email-helpers.js', 'utf8');
|
||||||
|
const globalScope = {};
|
||||||
|
|
||||||
|
const api = new Function('self', `${source}; return self.MultiPageGeneratedEmailHelpers;`)(globalScope);
|
||||||
|
|
||||||
|
assert.equal(typeof api?.createGeneratedEmailHelpers, 'function');
|
||||||
|
});
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
const test = require('node:test');
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
|
||||||
|
test('background imports panel bridge module', () => {
|
||||||
|
const source = fs.readFileSync('background.js', 'utf8');
|
||||||
|
assert.match(source, /importScripts\([\s\S]*'background\/panel-bridge\.js'/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('panel bridge module exposes a factory', () => {
|
||||||
|
const source = fs.readFileSync('background/panel-bridge.js', 'utf8');
|
||||||
|
const globalScope = {};
|
||||||
|
|
||||||
|
const api = new Function('self', `${source}; return self.MultiPageBackgroundPanelBridge;`)(globalScope);
|
||||||
|
|
||||||
|
assert.equal(typeof api?.createPanelBridge, 'function');
|
||||||
|
});
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
const test = require('node:test');
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
|
||||||
|
test('background imports signup flow helper module', () => {
|
||||||
|
const source = fs.readFileSync('background.js', 'utf8');
|
||||||
|
assert.match(source, /importScripts\([\s\S]*'background\/signup-flow-helpers\.js'/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('signup flow helper module exposes a factory', () => {
|
||||||
|
const source = fs.readFileSync('background/signup-flow-helpers.js', 'utf8');
|
||||||
|
const globalScope = {};
|
||||||
|
|
||||||
|
const api = new Function('self', `${source}; return self.MultiPageSignupFlowHelpers;`)(globalScope);
|
||||||
|
|
||||||
|
assert.equal(typeof api?.createSignupFlowHelpers, 'function');
|
||||||
|
});
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
const test = require('node:test');
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
|
||||||
|
test('sidepanel loads hotmail manager before sidepanel bootstrap', () => {
|
||||||
|
const html = fs.readFileSync('sidepanel/sidepanel.html', 'utf8');
|
||||||
|
const hotmailManagerIndex = html.indexOf('<script src="hotmail-manager.js"></script>');
|
||||||
|
const sidepanelIndex = html.indexOf('<script src="sidepanel.js"></script>');
|
||||||
|
|
||||||
|
assert.notEqual(hotmailManagerIndex, -1);
|
||||||
|
assert.notEqual(sidepanelIndex, -1);
|
||||||
|
assert.ok(hotmailManagerIndex < sidepanelIndex);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('hotmail manager exposes a factory and renders empty state', () => {
|
||||||
|
const source = fs.readFileSync('sidepanel/hotmail-manager.js', 'utf8');
|
||||||
|
const windowObject = {};
|
||||||
|
const localStorageMock = {
|
||||||
|
getItem() {
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
setItem() {},
|
||||||
|
};
|
||||||
|
|
||||||
|
const api = new Function('window', 'localStorage', `${source}; return window.SidepanelHotmailManager;`)(
|
||||||
|
windowObject,
|
||||||
|
localStorageMock
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(typeof api?.createHotmailManager, 'function');
|
||||||
|
|
||||||
|
const hotmailAccountsList = { innerHTML: '' };
|
||||||
|
const toggleButton = {
|
||||||
|
textContent: '',
|
||||||
|
disabled: false,
|
||||||
|
setAttribute() {},
|
||||||
|
};
|
||||||
|
const noopClassList = { toggle() {} };
|
||||||
|
|
||||||
|
const manager = api.createHotmailManager({
|
||||||
|
state: {
|
||||||
|
getLatestState: () => ({ currentHotmailAccountId: null }),
|
||||||
|
syncLatestState() {},
|
||||||
|
},
|
||||||
|
dom: {
|
||||||
|
btnClearUsedHotmailAccounts: { textContent: '', disabled: false },
|
||||||
|
btnDeleteAllHotmailAccounts: { textContent: '', disabled: false },
|
||||||
|
btnToggleHotmailList: toggleButton,
|
||||||
|
hotmailAccountsList,
|
||||||
|
hotmailListShell: { classList: noopClassList },
|
||||||
|
selectMailProvider: { value: 'hotmail-api' },
|
||||||
|
inputEmail: { value: '' },
|
||||||
|
},
|
||||||
|
helpers: {
|
||||||
|
getHotmailAccounts: () => [],
|
||||||
|
getCurrentHotmailEmail: () => '',
|
||||||
|
escapeHtml: (value) => String(value || ''),
|
||||||
|
showToast() {},
|
||||||
|
openConfirmModal: async () => true,
|
||||||
|
copyTextToClipboard: async () => {},
|
||||||
|
},
|
||||||
|
runtime: {
|
||||||
|
sendMessage: async () => ({}),
|
||||||
|
},
|
||||||
|
constants: {
|
||||||
|
copyIcon: '',
|
||||||
|
displayTimeZone: 'Asia/Shanghai',
|
||||||
|
expandedStorageKey: 'multipage-hotmail-list-expanded',
|
||||||
|
},
|
||||||
|
hotmailUtils: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(typeof manager.renderHotmailAccounts, 'function');
|
||||||
|
assert.equal(typeof manager.bindHotmailEvents, 'function');
|
||||||
|
assert.equal(typeof manager.initHotmailListExpandedState, 'function');
|
||||||
|
|
||||||
|
manager.renderHotmailAccounts();
|
||||||
|
assert.match(hotmailAccountsList.innerHTML, /还没有 Hotmail 账号/);
|
||||||
|
});
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
const test = require('node:test');
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
|
||||||
|
test('sidepanel loads icloud manager before sidepanel bootstrap', () => {
|
||||||
|
const html = fs.readFileSync('sidepanel/sidepanel.html', 'utf8');
|
||||||
|
const icloudManagerIndex = html.indexOf('<script src="icloud-manager.js"></script>');
|
||||||
|
const sidepanelIndex = html.indexOf('<script src="sidepanel.js"></script>');
|
||||||
|
|
||||||
|
assert.notEqual(icloudManagerIndex, -1);
|
||||||
|
assert.notEqual(sidepanelIndex, -1);
|
||||||
|
assert.ok(icloudManagerIndex < sidepanelIndex);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('icloud manager exposes a factory and renders empty state', () => {
|
||||||
|
const source = fs.readFileSync('sidepanel/icloud-manager.js', 'utf8');
|
||||||
|
const windowObject = {};
|
||||||
|
|
||||||
|
const api = new Function('window', `${source}; return window.SidepanelIcloudManager;`)(windowObject);
|
||||||
|
|
||||||
|
assert.equal(typeof api?.createIcloudManager, 'function');
|
||||||
|
|
||||||
|
const manager = api.createIcloudManager({
|
||||||
|
dom: {
|
||||||
|
btnIcloudBulkDelete: { disabled: false },
|
||||||
|
btnIcloudBulkPreserve: { disabled: false },
|
||||||
|
btnIcloudBulkUnpreserve: { disabled: false },
|
||||||
|
btnIcloudBulkUnused: { disabled: false },
|
||||||
|
btnIcloudBulkUsed: { disabled: false },
|
||||||
|
btnIcloudDeleteUsed: { disabled: false },
|
||||||
|
btnIcloudLoginDone: { disabled: false },
|
||||||
|
btnIcloudRefresh: { disabled: false },
|
||||||
|
checkboxIcloudSelectAll: { checked: false, indeterminate: false, disabled: false },
|
||||||
|
icloudList: { innerHTML: '' },
|
||||||
|
icloudLoginHelp: { style: { display: 'none' } },
|
||||||
|
icloudLoginHelpText: { textContent: '' },
|
||||||
|
icloudLoginHelpTitle: { textContent: '' },
|
||||||
|
icloudSection: { style: { display: '' } },
|
||||||
|
icloudSelectionSummary: { textContent: '' },
|
||||||
|
icloudSummary: { textContent: '' },
|
||||||
|
inputIcloudSearch: { value: '', disabled: false },
|
||||||
|
selectIcloudFilter: { value: 'all', disabled: false },
|
||||||
|
},
|
||||||
|
helpers: {
|
||||||
|
escapeHtml: (value) => String(value || ''),
|
||||||
|
openConfirmModal: async () => true,
|
||||||
|
showToast() {},
|
||||||
|
},
|
||||||
|
runtime: {
|
||||||
|
sendMessage: async () => ({ aliases: [] }),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(typeof manager.renderIcloudAliases, 'function');
|
||||||
|
assert.equal(typeof manager.refreshIcloudAliases, 'function');
|
||||||
|
assert.equal(typeof manager.queueIcloudAliasRefresh, 'function');
|
||||||
|
assert.equal(typeof manager.reset, 'function');
|
||||||
|
|
||||||
|
manager.renderIcloudAliases([]);
|
||||||
|
assert.equal(manager.hasDeletableUsedAliases(), false);
|
||||||
|
});
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
const test = require('node:test');
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
|
||||||
|
test('sidepanel loads luckmail manager before sidepanel bootstrap', () => {
|
||||||
|
const html = fs.readFileSync('sidepanel/sidepanel.html', 'utf8');
|
||||||
|
const luckmailManagerIndex = html.indexOf('<script src="luckmail-manager.js"></script>');
|
||||||
|
const sidepanelIndex = html.indexOf('<script src="sidepanel.js"></script>');
|
||||||
|
|
||||||
|
assert.notEqual(luckmailManagerIndex, -1);
|
||||||
|
assert.notEqual(sidepanelIndex, -1);
|
||||||
|
assert.ok(luckmailManagerIndex < sidepanelIndex);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('luckmail manager exposes a factory and renders empty state', () => {
|
||||||
|
const source = fs.readFileSync('sidepanel/luckmail-manager.js', 'utf8');
|
||||||
|
const windowObject = {};
|
||||||
|
|
||||||
|
const api = new Function('window', `${source}; return window.SidepanelLuckmailManager;`)(windowObject);
|
||||||
|
|
||||||
|
assert.equal(typeof api?.createLuckmailManager, 'function');
|
||||||
|
|
||||||
|
const manager = api.createLuckmailManager({
|
||||||
|
dom: {
|
||||||
|
btnLuckmailBulkDisable: { disabled: false },
|
||||||
|
btnLuckmailBulkEnable: { disabled: false },
|
||||||
|
btnLuckmailBulkPreserve: { disabled: false },
|
||||||
|
btnLuckmailBulkUnpreserve: { disabled: false },
|
||||||
|
btnLuckmailBulkUnused: { disabled: false },
|
||||||
|
btnLuckmailBulkUsed: { disabled: false },
|
||||||
|
btnLuckmailDisableUsed: { disabled: false, textContent: '' },
|
||||||
|
btnLuckmailRefresh: { disabled: false },
|
||||||
|
checkboxLuckmailSelectAll: { checked: false, indeterminate: false, disabled: false },
|
||||||
|
inputEmail: { value: '' },
|
||||||
|
inputLuckmailSearch: { value: '', disabled: false },
|
||||||
|
luckmailList: { innerHTML: '' },
|
||||||
|
luckmailSection: { style: { display: '' } },
|
||||||
|
luckmailSelectionSummary: { textContent: '' },
|
||||||
|
luckmailSummary: { textContent: '' },
|
||||||
|
selectLuckmailFilter: { value: 'all', disabled: false },
|
||||||
|
},
|
||||||
|
helpers: {
|
||||||
|
copyTextToClipboard: async () => {},
|
||||||
|
escapeHtml: (value) => String(value || ''),
|
||||||
|
formatLuckmailDateTime: (value) => String(value || ''),
|
||||||
|
getLuckmailPreserveTagName: () => '保留',
|
||||||
|
normalizeLuckmailProjectName: (value) => String(value || '').trim().toLowerCase(),
|
||||||
|
openConfirmModal: async () => true,
|
||||||
|
showToast() {},
|
||||||
|
},
|
||||||
|
runtime: {
|
||||||
|
sendMessage: async () => ({ purchases: [] }),
|
||||||
|
},
|
||||||
|
constants: {
|
||||||
|
copyIcon: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(typeof manager.renderLuckmailPurchases, 'function');
|
||||||
|
assert.equal(typeof manager.refreshLuckmailPurchases, 'function');
|
||||||
|
assert.equal(typeof manager.queueLuckmailPurchaseRefresh, 'function');
|
||||||
|
assert.equal(typeof manager.reset, 'function');
|
||||||
|
|
||||||
|
manager.renderLuckmailPurchases([]);
|
||||||
|
manager.reset();
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user