| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798 |
- (function attachBackgroundTabRuntime(root, factory) {
- root.MultiPageBackgroundTabRuntime = factory();
- })(typeof self !== 'undefined' ? self : globalThis, function createBackgroundTabRuntimeModule() {
- function createTabRuntime(deps = {}) {
- const {
- addLog,
- chrome,
- getSourceLabel,
- getState,
- isLocalhostOAuthCallbackUrl,
- isRetryableContentScriptTransportError,
- LOG_PREFIX,
- matchesSourceUrlFamily,
- setState,
- sleepWithStop,
- STOP_ERROR_MESSAGE,
- throwIfStopped,
- } = deps;
- const pendingCommands = new Map();
- async function sleepOrStop(ms) {
- if (typeof sleepWithStop === 'function') {
- await sleepWithStop(ms);
- return;
- }
- const start = Date.now();
- while (Date.now() - start < ms) {
- throwIfStopped();
- await new Promise((resolve) => setTimeout(resolve, Math.min(100, ms - (Date.now() - start))));
- }
- }
- function waitForTabUpdateComplete(tabId, timeoutMs = 30000) {
- return new Promise((resolve, reject) => {
- let settled = false;
- let stopTimer = null;
- const cleanup = () => {
- if (settled) return;
- settled = true;
- clearTimeout(timer);
- clearTimeout(stopTimer);
- chrome.tabs.onUpdated.removeListener(listener);
- };
- const resolveSafely = () => {
- cleanup();
- resolve();
- };
- const rejectSafely = (error) => {
- cleanup();
- reject(error);
- };
- const listener = (updatedTabId, info) => {
- if (updatedTabId === tabId && info.status === 'complete') {
- resolveSafely();
- }
- };
- const timer = setTimeout(resolveSafely, timeoutMs);
- chrome.tabs.onUpdated.addListener(listener);
- const pollStop = () => {
- if (settled) return;
- try {
- throwIfStopped();
- } catch (error) {
- rejectSafely(error);
- return;
- }
- stopTimer = setTimeout(pollStop, 100);
- };
- pollStop();
- });
- }
- async function getTabRegistry() {
- const state = await getState();
- return state.tabRegistry || {};
- }
- async function registerTab(source, tabId) {
- const registry = await getTabRegistry();
- registry[source] = { tabId, ready: true };
- await setState({ tabRegistry: registry });
- console.log(LOG_PREFIX, `Tab registered: ${source} -> ${tabId}`);
- }
- async function isTabAlive(source) {
- const registry = await getTabRegistry();
- const entry = registry[source];
- if (!entry) return false;
- try {
- await chrome.tabs.get(entry.tabId);
- return true;
- } catch {
- registry[source] = null;
- await setState({ tabRegistry: registry });
- return false;
- }
- }
- async function getTabId(source) {
- const registry = await getTabRegistry();
- return registry[source]?.tabId || null;
- }
- async function rememberSourceLastUrl(source, url) {
- if (!source || !url) return;
- const state = await getState();
- const sourceLastUrls = { ...(state.sourceLastUrls || {}) };
- sourceLastUrls[source] = url;
- await setState({ sourceLastUrls });
- }
- async function findReusableTabsForSource(source, referenceUrl, options = {}) {
- const { excludeTabIds = [] } = options;
- const excluded = new Set(excludeTabIds.filter((id) => Number.isInteger(id)));
- const tabs = await chrome.tabs.query({});
- return tabs
- .filter((tab) => Number.isInteger(tab.id) && !excluded.has(tab.id))
- .filter((tab) => matchesSourceUrlFamily(source, tab.url, referenceUrl));
- }
- function sortReusableTabsByPriority(tabs = [], referenceUrl = '') {
- return [...tabs].sort((left, right) => {
- const leftExact = left?.url === referenceUrl ? 1 : 0;
- const rightExact = right?.url === referenceUrl ? 1 : 0;
- if (leftExact !== rightExact) return rightExact - leftExact;
- const leftActive = left?.active ? 1 : 0;
- const rightActive = right?.active ? 1 : 0;
- if (leftActive !== rightActive) return rightActive - leftActive;
- return Number(left?.id || 0) - Number(right?.id || 0);
- });
- }
- async function closeConflictingTabsForSource(source, currentUrl, options = {}) {
- const { excludeTabIds = [] } = options;
- const excluded = new Set(excludeTabIds.filter((id) => Number.isInteger(id)));
- const state = await getState();
- const lastUrl = state.sourceLastUrls?.[source];
- const referenceUrls = [currentUrl, lastUrl].filter(Boolean);
- if (!referenceUrls.length) return;
- const tabs = await chrome.tabs.query({});
- const matchedIds = tabs
- .filter((tab) => Number.isInteger(tab.id) && !excluded.has(tab.id))
- .filter((tab) => referenceUrls.some((refUrl) => matchesSourceUrlFamily(source, tab.url, refUrl)))
- .map((tab) => tab.id);
- if (!matchedIds.length) return;
- await chrome.tabs.remove(matchedIds).catch(() => { });
- const registry = await getTabRegistry();
- if (registry[source]?.tabId && matchedIds.includes(registry[source].tabId)) {
- registry[source] = null;
- await setState({ tabRegistry: registry });
- }
- await addLog(`已关闭 ${matchedIds.length} 个旧的${getSourceLabel(source)}标签页。`, 'info');
- }
- function isLocalhostOAuthCallbackTabMatch(callbackUrl, candidateUrl) {
- if (!isLocalhostOAuthCallbackUrl(callbackUrl) || !isLocalhostOAuthCallbackUrl(candidateUrl)) {
- return false;
- }
- const callback = new URL(callbackUrl);
- const candidate = new URL(candidateUrl);
- return callback.origin === candidate.origin
- && callback.pathname === candidate.pathname
- && callback.searchParams.get('code') === candidate.searchParams.get('code')
- && callback.searchParams.get('state') === candidate.searchParams.get('state');
- }
- async function closeLocalhostCallbackTabs(callbackUrl, options = {}) {
- if (!isLocalhostOAuthCallbackUrl(callbackUrl)) return 0;
- const { excludeTabIds = [] } = options;
- const excluded = new Set(excludeTabIds.filter((id) => Number.isInteger(id)));
- const tabs = await chrome.tabs.query({});
- const matchedIds = tabs
- .filter((tab) => Number.isInteger(tab.id) && !excluded.has(tab.id))
- .filter((tab) => isLocalhostOAuthCallbackTabMatch(callbackUrl, tab.url))
- .map((tab) => tab.id);
- if (!matchedIds.length) return 0;
- await chrome.tabs.remove(matchedIds).catch(() => { });
- const registry = await getTabRegistry();
- if (registry['signup-page']?.tabId && matchedIds.includes(registry['signup-page'].tabId)) {
- registry['signup-page'] = null;
- await setState({ tabRegistry: registry });
- }
- await addLog(`已关闭 ${matchedIds.length} 个匹配当前 OAuth callback 的 localhost 残留标签页。`, 'info');
- return matchedIds.length;
- }
- function buildLocalhostCleanupPrefix(rawUrl) {
- if (!isLocalhostOAuthCallbackUrl(rawUrl)) return '';
- const parsed = new URL(rawUrl);
- const segments = parsed.pathname.split('/').filter(Boolean);
- if (!segments.length) return parsed.origin;
- return `${parsed.origin}/${segments[0]}`;
- }
- async function closeTabsByUrlPrefix(prefix, options = {}) {
- if (!prefix) return 0;
- const { excludeTabIds = [], excludeUrls = [], excludeLocalhostCallbacks = false } = options;
- const excluded = new Set(excludeTabIds.filter((id) => Number.isInteger(id)));
- const excludedUrls = new Set((Array.isArray(excludeUrls) ? excludeUrls : []).filter(Boolean));
- const tabs = await chrome.tabs.query({});
- const matchedIds = tabs
- .filter((tab) => Number.isInteger(tab.id) && !excluded.has(tab.id))
- .filter((tab) => typeof tab.url === 'string' && !excludedUrls.has(tab.url))
- .filter((tab) => !(excludeLocalhostCallbacks && isLocalhostOAuthCallbackUrl(tab.url)))
- .filter((tab) => typeof tab.url === 'string' && tab.url.startsWith(prefix))
- .filter((tab) => !isLocalhostOAuthCallbackUrl(tab.url))
- .map((tab) => tab.id);
- if (!matchedIds.length) return 0;
- await chrome.tabs.remove(matchedIds).catch(() => { });
- await addLog(`已关闭 ${matchedIds.length} 个匹配 ${prefix} 的 localhost 残留标签页。`, 'info');
- return matchedIds.length;
- }
- async function pingContentScriptOnTab(tabId) {
- if (!Number.isInteger(tabId)) return null;
- try {
- return await chrome.tabs.sendMessage(tabId, {
- type: 'PING',
- source: 'background',
- payload: {},
- });
- } catch {
- return null;
- }
- }
- async function waitForTabUrlFamily(source, tabId, referenceUrl, options = {}) {
- const { timeoutMs = 15000, retryDelayMs = 400 } = options;
- const start = Date.now();
- while (Date.now() - start < timeoutMs) {
- try {
- const tab = await chrome.tabs.get(tabId);
- if (matchesSourceUrlFamily(source, tab.url, referenceUrl)) {
- return tab;
- }
- } catch {
- return null;
- }
- await sleepOrStop(retryDelayMs);
- }
- return null;
- }
- async function waitForTabUrlMatch(tabId, matcher, options = {}) {
- const { timeoutMs = 15000, retryDelayMs = 400 } = options;
- const start = Date.now();
- while (Date.now() - start < timeoutMs) {
- try {
- const tab = await chrome.tabs.get(tabId);
- if (matcher(tab.url || '', tab)) {
- return tab;
- }
- } catch {
- return null;
- }
- await sleepOrStop(retryDelayMs);
- }
- return null;
- }
- async function waitForTabComplete(tabId, options = {}) {
- const { timeoutMs = 15000, retryDelayMs = 300 } = options;
- const start = Date.now();
- while (Date.now() - start < timeoutMs) {
- try {
- const tab = await chrome.tabs.get(tabId);
- if (tab?.status === 'complete') {
- return tab;
- }
- } catch {
- return null;
- }
- await sleepOrStop(retryDelayMs);
- }
- try {
- return await chrome.tabs.get(tabId);
- } catch {
- return null;
- }
- }
- async function ensureContentScriptReadyOnTab(source, tabId, options = {}) {
- const {
- inject = null,
- injectSource = null,
- timeoutMs = 30000,
- retryDelayMs = 700,
- logMessage = '',
- } = options;
- const start = Date.now();
- let lastError = null;
- let logged = false;
- let attempt = 0;
- console.log(
- LOG_PREFIX,
- `[ensureContentScriptReadyOnTab] start ${source} tab=${tabId}, timeout=${timeoutMs}ms, inject=${Array.isArray(inject) ? inject.join(',') : 'none'}`
- );
- while (Date.now() - start < timeoutMs) {
- attempt += 1;
- const pong = await pingContentScriptOnTab(tabId);
- if (pong?.ok && (!pong.source || pong.source === source)) {
- console.log(LOG_PREFIX, `[ensureContentScriptReadyOnTab] ready ${source} tab=${tabId} on attempt ${attempt} after ${Date.now() - start}ms`);
- await registerTab(source, tabId);
- return;
- }
- if (!inject || !inject.length) {
- throw new Error(`${getSourceLabel(source)} 内容脚本未就绪,且未提供可用的注入文件。`);
- }
- const registry = await getTabRegistry();
- if (registry[source]) {
- registry[source].ready = false;
- await setState({ tabRegistry: registry });
- }
- try {
- if (injectSource) {
- await chrome.scripting.executeScript({
- target: { tabId },
- func: (injectedSource) => {
- window.__MULTIPAGE_SOURCE = injectedSource;
- },
- args: [injectSource],
- });
- }
- await chrome.scripting.executeScript({
- target: { tabId },
- files: inject,
- });
- } catch (err) {
- lastError = err;
- console.warn(LOG_PREFIX, `[ensureContentScriptReadyOnTab] inject attempt ${attempt} failed for ${source} tab=${tabId}: ${err?.message || err}`);
- }
- const pongAfterInject = await pingContentScriptOnTab(tabId);
- if (pongAfterInject?.ok && (!pongAfterInject.source || pongAfterInject.source === source)) {
- console.log(LOG_PREFIX, `[ensureContentScriptReadyOnTab] ready after inject ${source} tab=${tabId} on attempt ${attempt} after ${Date.now() - start}ms`);
- await registerTab(source, tabId);
- return;
- }
- if (logMessage && !logged) {
- console.warn(LOG_PREFIX, `[ensureContentScriptReadyOnTab] ${source} tab=${tabId} still not ready after ${Date.now() - start}ms`);
- await addLog(logMessage, 'warn');
- logged = true;
- }
- await sleepOrStop(retryDelayMs);
- }
- throw lastError || new Error(`${getSourceLabel(source)} 内容脚本长时间未就绪。`);
- }
- function getContentScriptResponseTimeoutMs(message) {
- if (!message || typeof message !== 'object') return 30000;
- if (message.type === 'EXECUTE_STEP' && Number(message.step) === 6) return 75000;
- if (message.type === 'POLL_EMAIL') {
- const maxAttempts = Math.max(1, Number(message.payload?.maxAttempts) || 1);
- const intervalMs = Math.max(0, Number(message.payload?.intervalMs) || 0);
- return Math.max(45000, maxAttempts * intervalMs + 25000);
- }
- if (message.type === 'FILL_CODE') return Number(message.step) === 7 ? 45000 : 30000;
- if (message.type === 'PREPARE_SIGNUP_VERIFICATION') return 45000;
- return 30000;
- }
- function getMessageDebugLabel(source, message, tabId = null) {
- const parts = [source || 'unknown', message?.type || 'UNKNOWN'];
- if (Number.isInteger(message?.step)) parts.push(`step=${message.step}`);
- if (Number.isInteger(tabId)) parts.push(`tab=${tabId}`);
- return parts.join(' ');
- }
- function summarizeMessageResultForDebug(result) {
- if (result === undefined) return 'undefined';
- if (result === null) return 'null';
- if (typeof result !== 'object') return JSON.stringify(result);
- const summary = {};
- for (const key of ['ok', 'error', 'stopped', 'source', 'step']) {
- if (key in result) summary[key] = result[key];
- }
- if (result.payload && typeof result.payload === 'object') {
- summary.payloadKeys = Object.keys(result.payload);
- }
- return JSON.stringify(summary);
- }
- function sendTabMessageWithTimeout(tabId, source, message, responseTimeoutMs = getContentScriptResponseTimeoutMs(message)) {
- return new Promise((resolve, reject) => {
- let settled = false;
- const startedAt = Date.now();
- const debugLabel = getMessageDebugLabel(source, message, tabId);
- console.log(LOG_PREFIX, `[sendTabMessageWithTimeout] dispatch ${debugLabel}, timeout=${responseTimeoutMs}ms`);
- const timer = setTimeout(() => {
- if (settled) return;
- settled = true;
- const seconds = Math.ceil(responseTimeoutMs / 1000);
- console.warn(LOG_PREFIX, `[sendTabMessageWithTimeout] timeout ${debugLabel} after ${Date.now() - startedAt}ms`);
- reject(new Error(`Content script on ${source} did not respond in ${seconds}s. Try refreshing the tab and retry.`));
- }, responseTimeoutMs);
- chrome.tabs.sendMessage(tabId, message)
- .then((value) => {
- const elapsed = Date.now() - startedAt;
- if (settled) return;
- settled = true;
- clearTimeout(timer);
- console.log(LOG_PREFIX, `[sendTabMessageWithTimeout] response ${debugLabel} after ${elapsed}ms: ${summarizeMessageResultForDebug(value)}`);
- resolve(value);
- })
- .catch((error) => {
- const elapsed = Date.now() - startedAt;
- if (settled) return;
- settled = true;
- clearTimeout(timer);
- console.warn(LOG_PREFIX, `[sendTabMessageWithTimeout] rejection ${debugLabel} after ${elapsed}ms: ${error?.message || error}`);
- reject(error);
- });
- });
- }
- function queueCommand(source, message, timeout = 15000) {
- return new Promise((resolve, reject) => {
- const timer = setTimeout(() => {
- pendingCommands.delete(source);
- reject(new Error(`Content script on ${source} did not respond in ${timeout / 1000}s. Try refreshing the tab and retry.`));
- }, timeout);
- pendingCommands.set(source, { message, resolve, reject, timer });
- console.log(LOG_PREFIX, `Command queued for ${source} (waiting for ready)`);
- });
- }
- function flushCommand(source, tabId) {
- const pending = pendingCommands.get(source);
- if (pending) {
- clearTimeout(pending.timer);
- pendingCommands.delete(source);
- sendTabMessageWithTimeout(tabId, source, pending.message).then(pending.resolve).catch(pending.reject);
- console.log(LOG_PREFIX, `Flushed queued command to ${source} (tab ${tabId})`);
- }
- }
- function cancelPendingCommands(reason = STOP_ERROR_MESSAGE) {
- for (const [source, pending] of pendingCommands.entries()) {
- clearTimeout(pending.timer);
- pending.reject(new Error(reason));
- pendingCommands.delete(source);
- console.log(LOG_PREFIX, `Cancelled queued command for ${source}`);
- }
- }
- async function reuseOrCreateTab(source, url, options = {}) {
- const alive = await isTabAlive(source);
- if (alive) {
- const tabId = await getTabId(source);
- await closeConflictingTabsForSource(source, url, { excludeTabIds: [tabId] });
- const currentTab = await chrome.tabs.get(tabId);
- const sameUrl = currentTab.url === url;
- const shouldReloadOnReuse = sameUrl && options.reloadIfSameUrl;
- const registry = await getTabRegistry();
- if (sameUrl) {
- await chrome.tabs.update(tabId, { active: true });
- if (shouldReloadOnReuse) {
- if (registry[source]) registry[source].ready = false;
- await setState({ tabRegistry: registry });
- await chrome.tabs.reload(tabId);
- await waitForTabUpdateComplete(tabId);
- }
- if (options.inject) {
- if (registry[source]) registry[source].ready = false;
- await setState({ tabRegistry: registry });
- if (options.injectSource) {
- await chrome.scripting.executeScript({
- target: { tabId },
- func: (injectedSource) => {
- window.__MULTIPAGE_SOURCE = injectedSource;
- },
- args: [options.injectSource],
- });
- }
- await chrome.scripting.executeScript({
- target: { tabId },
- files: options.inject,
- });
- await sleepOrStop(500);
- }
- await rememberSourceLastUrl(source, url);
- return tabId;
- }
- if (registry[source]) registry[source].ready = false;
- await setState({ tabRegistry: registry });
- await chrome.tabs.update(tabId, { url, active: true });
- await waitForTabUpdateComplete(tabId);
- if (options.inject) {
- if (options.injectSource) {
- await chrome.scripting.executeScript({
- target: { tabId },
- func: (injectedSource) => {
- window.__MULTIPAGE_SOURCE = injectedSource;
- },
- args: [options.injectSource],
- });
- }
- await chrome.scripting.executeScript({
- target: { tabId },
- files: options.inject,
- });
- }
- await sleepOrStop(500);
- await rememberSourceLastUrl(source, url);
- return tabId;
- }
- const reusableTabs = sortReusableTabsByPriority(
- await findReusableTabsForSource(source, url),
- url
- );
- if (reusableTabs.length) {
- const [keeper] = reusableTabs;
- await registerTab(source, keeper.id);
- await closeConflictingTabsForSource(source, url, { excludeTabIds: [keeper.id] });
- const currentTab = await chrome.tabs.get(keeper.id);
- const sameUrl = currentTab.url === url;
- const shouldReloadOnReuse = sameUrl && options.reloadIfSameUrl;
- const registry = await getTabRegistry();
- if (sameUrl) {
- await chrome.tabs.update(keeper.id, { active: true });
- if (shouldReloadOnReuse) {
- if (registry[source]) registry[source].ready = false;
- await setState({ tabRegistry: registry });
- await chrome.tabs.reload(keeper.id);
- await waitForTabUpdateComplete(keeper.id);
- }
- if (options.inject) {
- if (registry[source]) registry[source].ready = false;
- await setState({ tabRegistry: registry });
- if (options.injectSource) {
- await chrome.scripting.executeScript({
- target: { tabId: keeper.id },
- func: (injectedSource) => {
- window.__MULTIPAGE_SOURCE = injectedSource;
- },
- args: [options.injectSource],
- });
- }
- await chrome.scripting.executeScript({
- target: { tabId: keeper.id },
- files: options.inject,
- });
- await sleepOrStop(500);
- }
- await rememberSourceLastUrl(source, url);
- return keeper.id;
- }
- if (registry[source]) registry[source].ready = false;
- await setState({ tabRegistry: registry });
- await chrome.tabs.update(keeper.id, { url, active: true });
- await waitForTabUpdateComplete(keeper.id);
- if (options.inject) {
- if (options.injectSource) {
- await chrome.scripting.executeScript({
- target: { tabId: keeper.id },
- func: (injectedSource) => {
- window.__MULTIPAGE_SOURCE = injectedSource;
- },
- args: [options.injectSource],
- });
- }
- await chrome.scripting.executeScript({
- target: { tabId: keeper.id },
- files: options.inject,
- });
- }
- await sleepOrStop(500);
- await rememberSourceLastUrl(source, url);
- return keeper.id;
- }
- await closeConflictingTabsForSource(source, url);
- const tab = await chrome.tabs.create({ url, active: true });
- if (options.inject) {
- await waitForTabUpdateComplete(tab.id);
- if (options.injectSource) {
- await chrome.scripting.executeScript({
- target: { tabId: tab.id },
- func: (injectedSource) => {
- window.__MULTIPAGE_SOURCE = injectedSource;
- },
- args: [options.injectSource],
- });
- }
- await chrome.scripting.executeScript({
- target: { tabId: tab.id },
- files: options.inject,
- });
- }
- await rememberSourceLastUrl(source, url);
- return tab.id;
- }
- async function sendToContentScript(source, message, options = {}) {
- throwIfStopped();
- const { responseTimeoutMs = getContentScriptResponseTimeoutMs(message) } = options;
- const registry = await getTabRegistry();
- const entry = registry[source];
- if (!entry || !entry.ready) {
- throwIfStopped();
- return queueCommand(source, message);
- }
- const alive = await isTabAlive(source);
- throwIfStopped();
- if (!alive) {
- return queueCommand(source, message);
- }
- throwIfStopped();
- return sendTabMessageWithTimeout(entry.tabId, source, message, responseTimeoutMs);
- }
- async function sendToContentScriptResilient(source, message, options = {}) {
- const {
- timeoutMs = 30000,
- retryDelayMs = 600,
- logMessage = '',
- responseTimeoutMs,
- } = options;
- const start = Date.now();
- let lastError = null;
- let logged = false;
- let attempt = 0;
- while (Date.now() - start < timeoutMs) {
- throwIfStopped();
- attempt += 1;
- try {
- return await sendToContentScript(
- source,
- message,
- responseTimeoutMs !== undefined ? { responseTimeoutMs } : {}
- );
- } catch (err) {
- const retryable = isRetryableContentScriptTransportError(err);
- if (!retryable) {
- throw err;
- }
- lastError = err;
- if (logMessage && !logged) {
- await addLog(logMessage, 'warn');
- logged = true;
- }
- await sleepOrStop(retryDelayMs);
- }
- }
- throw lastError || new Error(`等待 ${getSourceLabel(source)} 重新就绪超时。`);
- }
- async function sendToMailContentScriptResilient(mail, message, options = {}) {
- const {
- timeoutMs = 45000,
- maxRecoveryAttempts = 2,
- responseTimeoutMs,
- } = options;
- const start = Date.now();
- let lastError = null;
- let recoveries = 0;
- let logged = false;
- while (Date.now() - start < timeoutMs) {
- throwIfStopped();
- try {
- return await sendToContentScript(
- mail.source,
- message,
- responseTimeoutMs !== undefined ? { responseTimeoutMs } : {}
- );
- } catch (err) {
- if (!isRetryableContentScriptTransportError(err)) {
- throw err;
- }
- lastError = err;
- if (!logged) {
- await addLog(`步骤 ${message.step}:${mail.label} 页面通信异常,正在尝试让邮箱页重新就绪...`, 'warn');
- logged = true;
- }
- if (recoveries >= maxRecoveryAttempts) {
- break;
- }
- recoveries += 1;
- await reuseOrCreateTab(mail.source, mail.url, {
- inject: mail.inject,
- injectSource: mail.injectSource,
- reloadIfSameUrl: true,
- });
- await sleepOrStop(800);
- }
- }
- throw lastError || new Error(`${mail.label} 页面未能重新就绪。`);
- }
- return {
- buildLocalhostCleanupPrefix,
- cancelPendingCommands,
- closeConflictingTabsForSource,
- closeLocalhostCallbackTabs,
- closeTabsByUrlPrefix,
- ensureContentScriptReadyOnTab,
- flushCommand,
- getContentScriptResponseTimeoutMs,
- getMessageDebugLabel,
- getTabId,
- getTabRegistry,
- isLocalhostOAuthCallbackTabMatch,
- isTabAlive,
- pingContentScriptOnTab,
- queueCommand,
- registerTab,
- rememberSourceLastUrl,
- reuseOrCreateTab,
- sendTabMessageWithTimeout,
- sendToContentScript,
- sendToContentScriptResilient,
- sendToMailContentScriptResilient,
- summarizeMessageResultForDebug,
- waitForTabComplete,
- waitForTabUrlFamily,
- waitForTabUrlMatch,
- };
- }
- return {
- createTabRuntime,
- };
- });
|