tab-runtime.js 23 KB

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