tab-runtime.js 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671
  1. (function attachBackgroundTabRuntime(root, factory) {
  2. root.MultiPageBackgroundTabRuntime = factory();
  3. })(typeof self !== 'undefined' ? self : globalThis, function createBackgroundTabRuntimeModule() {
  4. function createTabRuntime(deps = {}) {
  5. const {
  6. addLog,
  7. chrome,
  8. getSourceLabel,
  9. getState,
  10. isLocalhostOAuthCallbackUrl,
  11. isRetryableContentScriptTransportError,
  12. LOG_PREFIX,
  13. matchesSourceUrlFamily,
  14. setState,
  15. STOP_ERROR_MESSAGE,
  16. throwIfStopped,
  17. } = deps;
  18. const pendingCommands = new Map();
  19. async function getTabRegistry() {
  20. const state = await getState();
  21. return state.tabRegistry || {};
  22. }
  23. async function registerTab(source, tabId) {
  24. const registry = await getTabRegistry();
  25. registry[source] = { tabId, ready: true };
  26. await setState({ tabRegistry: registry });
  27. console.log(LOG_PREFIX, `Tab registered: ${source} -> ${tabId}`);
  28. }
  29. async function isTabAlive(source) {
  30. const registry = await getTabRegistry();
  31. const entry = registry[source];
  32. if (!entry) return false;
  33. try {
  34. await chrome.tabs.get(entry.tabId);
  35. return true;
  36. } catch {
  37. registry[source] = null;
  38. await setState({ tabRegistry: registry });
  39. return false;
  40. }
  41. }
  42. async function getTabId(source) {
  43. const registry = await getTabRegistry();
  44. return registry[source]?.tabId || null;
  45. }
  46. async function rememberSourceLastUrl(source, url) {
  47. if (!source || !url) return;
  48. const state = await getState();
  49. const sourceLastUrls = { ...(state.sourceLastUrls || {}) };
  50. sourceLastUrls[source] = url;
  51. await setState({ sourceLastUrls });
  52. }
  53. async function closeConflictingTabsForSource(source, currentUrl, options = {}) {
  54. const { excludeTabIds = [] } = options;
  55. const excluded = new Set(excludeTabIds.filter((id) => Number.isInteger(id)));
  56. const state = await getState();
  57. const lastUrl = state.sourceLastUrls?.[source];
  58. const referenceUrls = [currentUrl, lastUrl].filter(Boolean);
  59. if (!referenceUrls.length) return;
  60. const tabs = await chrome.tabs.query({});
  61. const matchedIds = tabs
  62. .filter((tab) => Number.isInteger(tab.id) && !excluded.has(tab.id))
  63. .filter((tab) => referenceUrls.some((refUrl) => matchesSourceUrlFamily(source, tab.url, refUrl)))
  64. .map((tab) => tab.id);
  65. if (!matchedIds.length) return;
  66. await chrome.tabs.remove(matchedIds).catch(() => { });
  67. const registry = await getTabRegistry();
  68. if (registry[source]?.tabId && matchedIds.includes(registry[source].tabId)) {
  69. registry[source] = null;
  70. await setState({ tabRegistry: registry });
  71. }
  72. await addLog(`已关闭 ${matchedIds.length} 个旧的${getSourceLabel(source)}标签页。`, 'info');
  73. }
  74. function isLocalhostOAuthCallbackTabMatch(callbackUrl, candidateUrl) {
  75. if (!isLocalhostOAuthCallbackUrl(callbackUrl) || !isLocalhostOAuthCallbackUrl(candidateUrl)) {
  76. return false;
  77. }
  78. const callback = new URL(callbackUrl);
  79. const candidate = new URL(candidateUrl);
  80. return callback.origin === candidate.origin
  81. && callback.pathname === candidate.pathname
  82. && callback.searchParams.get('code') === candidate.searchParams.get('code')
  83. && callback.searchParams.get('state') === candidate.searchParams.get('state');
  84. }
  85. async function closeLocalhostCallbackTabs(callbackUrl, options = {}) {
  86. if (!isLocalhostOAuthCallbackUrl(callbackUrl)) return 0;
  87. const { excludeTabIds = [] } = options;
  88. const excluded = new Set(excludeTabIds.filter((id) => Number.isInteger(id)));
  89. const tabs = await chrome.tabs.query({});
  90. const matchedIds = tabs
  91. .filter((tab) => Number.isInteger(tab.id) && !excluded.has(tab.id))
  92. .filter((tab) => isLocalhostOAuthCallbackTabMatch(callbackUrl, tab.url))
  93. .map((tab) => tab.id);
  94. if (!matchedIds.length) return 0;
  95. await chrome.tabs.remove(matchedIds).catch(() => { });
  96. const registry = await getTabRegistry();
  97. if (registry['signup-page']?.tabId && matchedIds.includes(registry['signup-page'].tabId)) {
  98. registry['signup-page'] = null;
  99. await setState({ tabRegistry: registry });
  100. }
  101. await addLog(`已关闭 ${matchedIds.length} 个匹配当前 OAuth callback 的 localhost 残留标签页。`, 'info');
  102. return matchedIds.length;
  103. }
  104. function buildLocalhostCleanupPrefix(rawUrl) {
  105. if (!isLocalhostOAuthCallbackUrl(rawUrl)) return '';
  106. const parsed = new URL(rawUrl);
  107. const segments = parsed.pathname.split('/').filter(Boolean);
  108. if (!segments.length) return parsed.origin;
  109. return `${parsed.origin}/${segments[0]}`;
  110. }
  111. async function closeTabsByUrlPrefix(prefix, options = {}) {
  112. if (!prefix) return 0;
  113. const { excludeTabIds = [], excludeUrls = [], excludeLocalhostCallbacks = false } = options;
  114. const excluded = new Set(excludeTabIds.filter((id) => Number.isInteger(id)));
  115. const excludedUrls = new Set((Array.isArray(excludeUrls) ? excludeUrls : []).filter(Boolean));
  116. const tabs = await chrome.tabs.query({});
  117. const matchedIds = tabs
  118. .filter((tab) => Number.isInteger(tab.id) && !excluded.has(tab.id))
  119. .filter((tab) => typeof tab.url === 'string' && !excludedUrls.has(tab.url))
  120. .filter((tab) => !(excludeLocalhostCallbacks && isLocalhostOAuthCallbackUrl(tab.url)))
  121. .filter((tab) => typeof tab.url === 'string' && tab.url.startsWith(prefix))
  122. .filter((tab) => !isLocalhostOAuthCallbackUrl(tab.url))
  123. .map((tab) => tab.id);
  124. if (!matchedIds.length) return 0;
  125. await chrome.tabs.remove(matchedIds).catch(() => { });
  126. await addLog(`已关闭 ${matchedIds.length} 个匹配 ${prefix} 的 localhost 残留标签页。`, 'info');
  127. return matchedIds.length;
  128. }
  129. async function pingContentScriptOnTab(tabId) {
  130. if (!Number.isInteger(tabId)) return null;
  131. try {
  132. return await chrome.tabs.sendMessage(tabId, {
  133. type: 'PING',
  134. source: 'background',
  135. payload: {},
  136. });
  137. } catch {
  138. return null;
  139. }
  140. }
  141. async function waitForTabUrlFamily(source, tabId, referenceUrl, options = {}) {
  142. const { timeoutMs = 15000, retryDelayMs = 400 } = options;
  143. const start = Date.now();
  144. while (Date.now() - start < timeoutMs) {
  145. try {
  146. const tab = await chrome.tabs.get(tabId);
  147. if (matchesSourceUrlFamily(source, tab.url, referenceUrl)) {
  148. return tab;
  149. }
  150. } catch {
  151. return null;
  152. }
  153. await new Promise((resolve) => setTimeout(resolve, retryDelayMs));
  154. }
  155. return null;
  156. }
  157. async function waitForTabUrlMatch(tabId, matcher, options = {}) {
  158. const { timeoutMs = 15000, retryDelayMs = 400 } = options;
  159. const start = Date.now();
  160. while (Date.now() - start < timeoutMs) {
  161. try {
  162. const tab = await chrome.tabs.get(tabId);
  163. if (matcher(tab.url || '', tab)) {
  164. return tab;
  165. }
  166. } catch {
  167. return null;
  168. }
  169. await new Promise((resolve) => setTimeout(resolve, retryDelayMs));
  170. }
  171. return null;
  172. }
  173. async function waitForTabComplete(tabId, options = {}) {
  174. const { timeoutMs = 15000, retryDelayMs = 300 } = options;
  175. const start = Date.now();
  176. while (Date.now() - start < timeoutMs) {
  177. try {
  178. const tab = await chrome.tabs.get(tabId);
  179. if (tab?.status === 'complete') {
  180. return tab;
  181. }
  182. } catch {
  183. return null;
  184. }
  185. await new Promise((resolve) => setTimeout(resolve, retryDelayMs));
  186. }
  187. try {
  188. return await chrome.tabs.get(tabId);
  189. } catch {
  190. return null;
  191. }
  192. }
  193. async function ensureContentScriptReadyOnTab(source, tabId, options = {}) {
  194. const {
  195. inject = null,
  196. injectSource = null,
  197. timeoutMs = 30000,
  198. retryDelayMs = 700,
  199. logMessage = '',
  200. } = options;
  201. const start = Date.now();
  202. let lastError = null;
  203. let logged = false;
  204. let attempt = 0;
  205. console.log(
  206. LOG_PREFIX,
  207. `[ensureContentScriptReadyOnTab] start ${source} tab=${tabId}, timeout=${timeoutMs}ms, inject=${Array.isArray(inject) ? inject.join(',') : 'none'}`
  208. );
  209. while (Date.now() - start < timeoutMs) {
  210. attempt += 1;
  211. const pong = await pingContentScriptOnTab(tabId);
  212. if (pong?.ok && (!pong.source || pong.source === source)) {
  213. console.log(LOG_PREFIX, `[ensureContentScriptReadyOnTab] ready ${source} tab=${tabId} on attempt ${attempt} after ${Date.now() - start}ms`);
  214. await registerTab(source, tabId);
  215. return;
  216. }
  217. if (!inject || !inject.length) {
  218. throw new Error(`${getSourceLabel(source)} 内容脚本未就绪,且未提供可用的注入文件。`);
  219. }
  220. const registry = await getTabRegistry();
  221. if (registry[source]) {
  222. registry[source].ready = false;
  223. await setState({ tabRegistry: registry });
  224. }
  225. try {
  226. if (injectSource) {
  227. await chrome.scripting.executeScript({
  228. target: { tabId },
  229. func: (injectedSource) => {
  230. window.__MULTIPAGE_SOURCE = injectedSource;
  231. },
  232. args: [injectSource],
  233. });
  234. }
  235. await chrome.scripting.executeScript({
  236. target: { tabId },
  237. files: inject,
  238. });
  239. } catch (err) {
  240. lastError = err;
  241. console.warn(LOG_PREFIX, `[ensureContentScriptReadyOnTab] inject attempt ${attempt} failed for ${source} tab=${tabId}: ${err?.message || err}`);
  242. }
  243. const pongAfterInject = await pingContentScriptOnTab(tabId);
  244. if (pongAfterInject?.ok && (!pongAfterInject.source || pongAfterInject.source === source)) {
  245. console.log(LOG_PREFIX, `[ensureContentScriptReadyOnTab] ready after inject ${source} tab=${tabId} on attempt ${attempt} after ${Date.now() - start}ms`);
  246. await registerTab(source, tabId);
  247. return;
  248. }
  249. if (logMessage && !logged) {
  250. console.warn(LOG_PREFIX, `[ensureContentScriptReadyOnTab] ${source} tab=${tabId} still not ready after ${Date.now() - start}ms`);
  251. await addLog(logMessage, 'warn');
  252. logged = true;
  253. }
  254. await new Promise((resolve) => setTimeout(resolve, retryDelayMs));
  255. }
  256. throw lastError || new Error(`${getSourceLabel(source)} 内容脚本长时间未就绪。`);
  257. }
  258. function getContentScriptResponseTimeoutMs(message) {
  259. if (!message || typeof message !== 'object') return 30000;
  260. if (message.type === 'EXECUTE_STEP' && Number(message.step) === 6) return 75000;
  261. if (message.type === 'POLL_EMAIL') {
  262. const maxAttempts = Math.max(1, Number(message.payload?.maxAttempts) || 1);
  263. const intervalMs = Math.max(0, Number(message.payload?.intervalMs) || 0);
  264. return Math.max(45000, maxAttempts * intervalMs + 25000);
  265. }
  266. if (message.type === 'FILL_CODE') return Number(message.step) === 7 ? 45000 : 30000;
  267. if (message.type === 'PREPARE_SIGNUP_VERIFICATION') return 45000;
  268. return 30000;
  269. }
  270. function getMessageDebugLabel(source, message, tabId = null) {
  271. const parts = [source || 'unknown', message?.type || 'UNKNOWN'];
  272. if (Number.isInteger(message?.step)) parts.push(`step=${message.step}`);
  273. if (Number.isInteger(tabId)) parts.push(`tab=${tabId}`);
  274. return parts.join(' ');
  275. }
  276. function summarizeMessageResultForDebug(result) {
  277. if (result === undefined) return 'undefined';
  278. if (result === null) return 'null';
  279. if (typeof result !== 'object') return JSON.stringify(result);
  280. const summary = {};
  281. for (const key of ['ok', 'error', 'stopped', 'source', 'step']) {
  282. if (key in result) summary[key] = result[key];
  283. }
  284. if (result.payload && typeof result.payload === 'object') {
  285. summary.payloadKeys = Object.keys(result.payload);
  286. }
  287. return JSON.stringify(summary);
  288. }
  289. function sendTabMessageWithTimeout(tabId, source, message, responseTimeoutMs = getContentScriptResponseTimeoutMs(message)) {
  290. return new Promise((resolve, reject) => {
  291. let settled = false;
  292. const startedAt = Date.now();
  293. const debugLabel = getMessageDebugLabel(source, message, tabId);
  294. console.log(LOG_PREFIX, `[sendTabMessageWithTimeout] dispatch ${debugLabel}, timeout=${responseTimeoutMs}ms`);
  295. const timer = setTimeout(() => {
  296. if (settled) return;
  297. settled = true;
  298. const seconds = Math.ceil(responseTimeoutMs / 1000);
  299. console.warn(LOG_PREFIX, `[sendTabMessageWithTimeout] timeout ${debugLabel} after ${Date.now() - startedAt}ms`);
  300. reject(new Error(`Content script on ${source} did not respond in ${seconds}s. Try refreshing the tab and retry.`));
  301. }, responseTimeoutMs);
  302. chrome.tabs.sendMessage(tabId, message)
  303. .then((value) => {
  304. const elapsed = Date.now() - startedAt;
  305. if (settled) return;
  306. settled = true;
  307. clearTimeout(timer);
  308. console.log(LOG_PREFIX, `[sendTabMessageWithTimeout] response ${debugLabel} after ${elapsed}ms: ${summarizeMessageResultForDebug(value)}`);
  309. resolve(value);
  310. })
  311. .catch((error) => {
  312. const elapsed = Date.now() - startedAt;
  313. if (settled) return;
  314. settled = true;
  315. clearTimeout(timer);
  316. console.warn(LOG_PREFIX, `[sendTabMessageWithTimeout] rejection ${debugLabel} after ${elapsed}ms: ${error?.message || error}`);
  317. reject(error);
  318. });
  319. });
  320. }
  321. function queueCommand(source, message, timeout = 15000) {
  322. return new Promise((resolve, reject) => {
  323. const timer = setTimeout(() => {
  324. pendingCommands.delete(source);
  325. reject(new Error(`Content script on ${source} did not respond in ${timeout / 1000}s. Try refreshing the tab and retry.`));
  326. }, timeout);
  327. pendingCommands.set(source, { message, resolve, reject, timer });
  328. console.log(LOG_PREFIX, `Command queued for ${source} (waiting for ready)`);
  329. });
  330. }
  331. function flushCommand(source, tabId) {
  332. const pending = pendingCommands.get(source);
  333. if (pending) {
  334. clearTimeout(pending.timer);
  335. pendingCommands.delete(source);
  336. sendTabMessageWithTimeout(tabId, source, pending.message).then(pending.resolve).catch(pending.reject);
  337. console.log(LOG_PREFIX, `Flushed queued command to ${source} (tab ${tabId})`);
  338. }
  339. }
  340. function cancelPendingCommands(reason = STOP_ERROR_MESSAGE) {
  341. for (const [source, pending] of pendingCommands.entries()) {
  342. clearTimeout(pending.timer);
  343. pending.reject(new Error(reason));
  344. pendingCommands.delete(source);
  345. console.log(LOG_PREFIX, `Cancelled queued command for ${source}`);
  346. }
  347. }
  348. async function reuseOrCreateTab(source, url, options = {}) {
  349. const alive = await isTabAlive(source);
  350. if (alive) {
  351. const tabId = await getTabId(source);
  352. await closeConflictingTabsForSource(source, url, { excludeTabIds: [tabId] });
  353. const currentTab = await chrome.tabs.get(tabId);
  354. const sameUrl = currentTab.url === url;
  355. const shouldReloadOnReuse = sameUrl && options.reloadIfSameUrl;
  356. const registry = await getTabRegistry();
  357. if (sameUrl) {
  358. await chrome.tabs.update(tabId, { active: true });
  359. if (shouldReloadOnReuse) {
  360. if (registry[source]) registry[source].ready = false;
  361. await setState({ tabRegistry: registry });
  362. await chrome.tabs.reload(tabId);
  363. await new Promise((resolve) => {
  364. const timer = setTimeout(() => { chrome.tabs.onUpdated.removeListener(listener); resolve(); }, 30000);
  365. const listener = (tid, info) => {
  366. if (tid === tabId && info.status === 'complete') {
  367. chrome.tabs.onUpdated.removeListener(listener);
  368. clearTimeout(timer);
  369. resolve();
  370. }
  371. };
  372. chrome.tabs.onUpdated.addListener(listener);
  373. });
  374. }
  375. if (options.inject) {
  376. if (registry[source]) registry[source].ready = false;
  377. await setState({ tabRegistry: registry });
  378. if (options.injectSource) {
  379. await chrome.scripting.executeScript({
  380. target: { tabId },
  381. func: (injectedSource) => {
  382. window.__MULTIPAGE_SOURCE = injectedSource;
  383. },
  384. args: [options.injectSource],
  385. });
  386. }
  387. await chrome.scripting.executeScript({
  388. target: { tabId },
  389. files: options.inject,
  390. });
  391. await new Promise((resolve) => setTimeout(resolve, 500));
  392. }
  393. await rememberSourceLastUrl(source, url);
  394. return tabId;
  395. }
  396. if (registry[source]) registry[source].ready = false;
  397. await setState({ tabRegistry: registry });
  398. await chrome.tabs.update(tabId, { url, active: true });
  399. await new Promise((resolve) => {
  400. const timer = setTimeout(() => { chrome.tabs.onUpdated.removeListener(listener); resolve(); }, 30000);
  401. const listener = (tid, info) => {
  402. if (tid === tabId && info.status === 'complete') {
  403. chrome.tabs.onUpdated.removeListener(listener);
  404. clearTimeout(timer);
  405. resolve();
  406. }
  407. };
  408. chrome.tabs.onUpdated.addListener(listener);
  409. });
  410. if (options.inject) {
  411. if (options.injectSource) {
  412. await chrome.scripting.executeScript({
  413. target: { tabId },
  414. func: (injectedSource) => {
  415. window.__MULTIPAGE_SOURCE = injectedSource;
  416. },
  417. args: [options.injectSource],
  418. });
  419. }
  420. await chrome.scripting.executeScript({
  421. target: { tabId },
  422. files: options.inject,
  423. });
  424. }
  425. await new Promise((resolve) => setTimeout(resolve, 500));
  426. await rememberSourceLastUrl(source, url);
  427. return tabId;
  428. }
  429. await closeConflictingTabsForSource(source, url);
  430. const tab = await chrome.tabs.create({ url, active: true });
  431. if (options.inject) {
  432. await new Promise((resolve) => {
  433. const timer = setTimeout(() => { chrome.tabs.onUpdated.removeListener(listener); resolve(); }, 30000);
  434. const listener = (tabId, info) => {
  435. if (tabId === tab.id && info.status === 'complete') {
  436. chrome.tabs.onUpdated.removeListener(listener);
  437. clearTimeout(timer);
  438. resolve();
  439. }
  440. };
  441. chrome.tabs.onUpdated.addListener(listener);
  442. });
  443. if (options.injectSource) {
  444. await chrome.scripting.executeScript({
  445. target: { tabId: tab.id },
  446. func: (injectedSource) => {
  447. window.__MULTIPAGE_SOURCE = injectedSource;
  448. },
  449. args: [options.injectSource],
  450. });
  451. }
  452. await chrome.scripting.executeScript({
  453. target: { tabId: tab.id },
  454. files: options.inject,
  455. });
  456. }
  457. await rememberSourceLastUrl(source, url);
  458. return tab.id;
  459. }
  460. async function sendToContentScript(source, message, options = {}) {
  461. throwIfStopped();
  462. const { responseTimeoutMs = getContentScriptResponseTimeoutMs(message) } = options;
  463. const registry = await getTabRegistry();
  464. const entry = registry[source];
  465. if (!entry || !entry.ready) {
  466. throwIfStopped();
  467. return queueCommand(source, message);
  468. }
  469. const alive = await isTabAlive(source);
  470. throwIfStopped();
  471. if (!alive) {
  472. return queueCommand(source, message);
  473. }
  474. throwIfStopped();
  475. return sendTabMessageWithTimeout(entry.tabId, source, message, responseTimeoutMs);
  476. }
  477. async function sendToContentScriptResilient(source, message, options = {}) {
  478. const {
  479. timeoutMs = 30000,
  480. retryDelayMs = 600,
  481. logMessage = '',
  482. responseTimeoutMs,
  483. } = options;
  484. const start = Date.now();
  485. let lastError = null;
  486. let logged = false;
  487. let attempt = 0;
  488. while (Date.now() - start < timeoutMs) {
  489. throwIfStopped();
  490. attempt += 1;
  491. try {
  492. return await sendToContentScript(
  493. source,
  494. message,
  495. responseTimeoutMs !== undefined ? { responseTimeoutMs } : {}
  496. );
  497. } catch (err) {
  498. const retryable = isRetryableContentScriptTransportError(err);
  499. if (!retryable) {
  500. throw err;
  501. }
  502. lastError = err;
  503. if (logMessage && !logged) {
  504. await addLog(logMessage, 'warn');
  505. logged = true;
  506. }
  507. await new Promise((resolve) => setTimeout(resolve, retryDelayMs));
  508. }
  509. }
  510. throw lastError || new Error(`等待 ${getSourceLabel(source)} 重新就绪超时。`);
  511. }
  512. async function sendToMailContentScriptResilient(mail, message, options = {}) {
  513. const {
  514. timeoutMs = 45000,
  515. maxRecoveryAttempts = 2,
  516. responseTimeoutMs,
  517. } = options;
  518. const start = Date.now();
  519. let lastError = null;
  520. let recoveries = 0;
  521. let logged = false;
  522. while (Date.now() - start < timeoutMs) {
  523. throwIfStopped();
  524. try {
  525. return await sendToContentScript(
  526. mail.source,
  527. message,
  528. responseTimeoutMs !== undefined ? { responseTimeoutMs } : {}
  529. );
  530. } catch (err) {
  531. if (!isRetryableContentScriptTransportError(err)) {
  532. throw err;
  533. }
  534. lastError = err;
  535. if (!logged) {
  536. await addLog(`步骤 ${message.step}:${mail.label} 页面通信异常,正在尝试让邮箱页重新就绪...`, 'warn');
  537. logged = true;
  538. }
  539. if (recoveries >= maxRecoveryAttempts) {
  540. break;
  541. }
  542. recoveries += 1;
  543. await reuseOrCreateTab(mail.source, mail.url, {
  544. inject: mail.inject,
  545. injectSource: mail.injectSource,
  546. reloadIfSameUrl: true,
  547. });
  548. await new Promise((resolve) => setTimeout(resolve, 800));
  549. }
  550. }
  551. throw lastError || new Error(`${mail.label} 页面未能重新就绪。`);
  552. }
  553. return {
  554. buildLocalhostCleanupPrefix,
  555. cancelPendingCommands,
  556. closeConflictingTabsForSource,
  557. closeLocalhostCallbackTabs,
  558. closeTabsByUrlPrefix,
  559. ensureContentScriptReadyOnTab,
  560. flushCommand,
  561. getContentScriptResponseTimeoutMs,
  562. getMessageDebugLabel,
  563. getTabId,
  564. getTabRegistry,
  565. isLocalhostOAuthCallbackTabMatch,
  566. isTabAlive,
  567. pingContentScriptOnTab,
  568. queueCommand,
  569. registerTab,
  570. rememberSourceLastUrl,
  571. reuseOrCreateTab,
  572. sendTabMessageWithTimeout,
  573. sendToContentScript,
  574. sendToContentScriptResilient,
  575. sendToMailContentScriptResilient,
  576. summarizeMessageResultForDebug,
  577. waitForTabComplete,
  578. waitForTabUrlFamily,
  579. waitForTabUrlMatch,
  580. };
  581. }
  582. return {
  583. createTabRuntime,
  584. };
  585. });