tab-runtime.js 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845
  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 findReusableTabsForSource(source, referenceUrl, options = {}) {
  105. const { excludeTabIds = [] } = options;
  106. const excluded = new Set(excludeTabIds.filter((id) => Number.isInteger(id)));
  107. const tabs = await chrome.tabs.query({});
  108. return tabs
  109. .filter((tab) => Number.isInteger(tab.id) && !excluded.has(tab.id))
  110. .filter((tab) => matchesSourceUrlFamily(source, tab.url, referenceUrl));
  111. }
  112. function sortReusableTabsByPriority(tabs = [], referenceUrl = '') {
  113. return [...tabs].sort((left, right) => {
  114. const leftExact = left?.url === referenceUrl ? 1 : 0;
  115. const rightExact = right?.url === referenceUrl ? 1 : 0;
  116. if (leftExact !== rightExact) return rightExact - leftExact;
  117. const leftActive = left?.active ? 1 : 0;
  118. const rightActive = right?.active ? 1 : 0;
  119. if (leftActive !== rightActive) return rightActive - leftActive;
  120. return Number(left?.id || 0) - Number(right?.id || 0);
  121. });
  122. }
  123. async function closeConflictingTabsForSource(source, currentUrl, options = {}) {
  124. const { excludeTabIds = [] } = options;
  125. const excluded = new Set(excludeTabIds.filter((id) => Number.isInteger(id)));
  126. const state = await getState();
  127. const lastUrl = state.sourceLastUrls?.[source];
  128. const referenceUrls = [currentUrl, lastUrl].filter(Boolean);
  129. if (!referenceUrls.length) return;
  130. const tabs = await chrome.tabs.query({});
  131. const matchedIds = tabs
  132. .filter((tab) => Number.isInteger(tab.id) && !excluded.has(tab.id))
  133. .filter((tab) => referenceUrls.some((refUrl) => matchesSourceUrlFamily(source, tab.url, refUrl)))
  134. .map((tab) => tab.id);
  135. if (!matchedIds.length) return;
  136. await chrome.tabs.remove(matchedIds).catch(() => { });
  137. const registry = await getTabRegistry();
  138. if (registry[source]?.tabId && matchedIds.includes(registry[source].tabId)) {
  139. registry[source] = null;
  140. await setState({ tabRegistry: registry });
  141. }
  142. await addLog(`已关闭 ${matchedIds.length} 个旧的${getSourceLabel(source)}标签页。`, 'info');
  143. }
  144. function isLocalhostOAuthCallbackTabMatch(callbackUrl, candidateUrl) {
  145. if (!isLocalhostOAuthCallbackUrl(callbackUrl) || !isLocalhostOAuthCallbackUrl(candidateUrl)) {
  146. return false;
  147. }
  148. const callback = new URL(callbackUrl);
  149. const candidate = new URL(candidateUrl);
  150. return callback.origin === candidate.origin
  151. && callback.pathname === candidate.pathname
  152. && callback.searchParams.get('code') === candidate.searchParams.get('code')
  153. && callback.searchParams.get('state') === candidate.searchParams.get('state');
  154. }
  155. async function closeLocalhostCallbackTabs(callbackUrl, options = {}) {
  156. if (!isLocalhostOAuthCallbackUrl(callbackUrl)) return 0;
  157. const { excludeTabIds = [] } = options;
  158. const excluded = new Set(excludeTabIds.filter((id) => Number.isInteger(id)));
  159. const tabs = await chrome.tabs.query({});
  160. const matchedIds = tabs
  161. .filter((tab) => Number.isInteger(tab.id) && !excluded.has(tab.id))
  162. .filter((tab) => isLocalhostOAuthCallbackTabMatch(callbackUrl, tab.url))
  163. .map((tab) => tab.id);
  164. if (!matchedIds.length) return 0;
  165. await chrome.tabs.remove(matchedIds).catch(() => { });
  166. const registry = await getTabRegistry();
  167. if (registry['signup-page']?.tabId && matchedIds.includes(registry['signup-page'].tabId)) {
  168. registry['signup-page'] = null;
  169. await setState({ tabRegistry: registry });
  170. }
  171. await addLog(`已关闭 ${matchedIds.length} 个匹配当前 OAuth callback 的 localhost 残留标签页。`, 'info');
  172. return matchedIds.length;
  173. }
  174. function buildLocalhostCleanupPrefix(rawUrl) {
  175. if (!isLocalhostOAuthCallbackUrl(rawUrl)) return '';
  176. const parsed = new URL(rawUrl);
  177. const segments = parsed.pathname.split('/').filter(Boolean);
  178. if (!segments.length) return parsed.origin;
  179. return `${parsed.origin}/${segments[0]}`;
  180. }
  181. async function closeTabsByUrlPrefix(prefix, options = {}) {
  182. if (!prefix) return 0;
  183. const { excludeTabIds = [], excludeUrls = [], excludeLocalhostCallbacks = false } = options;
  184. const excluded = new Set(excludeTabIds.filter((id) => Number.isInteger(id)));
  185. const excludedUrls = new Set((Array.isArray(excludeUrls) ? excludeUrls : []).filter(Boolean));
  186. const tabs = await chrome.tabs.query({});
  187. const matchedIds = tabs
  188. .filter((tab) => Number.isInteger(tab.id) && !excluded.has(tab.id))
  189. .filter((tab) => typeof tab.url === 'string' && !excludedUrls.has(tab.url))
  190. .filter((tab) => !(excludeLocalhostCallbacks && isLocalhostOAuthCallbackUrl(tab.url)))
  191. .filter((tab) => typeof tab.url === 'string' && tab.url.startsWith(prefix))
  192. .filter((tab) => !isLocalhostOAuthCallbackUrl(tab.url))
  193. .map((tab) => tab.id);
  194. if (!matchedIds.length) return 0;
  195. await chrome.tabs.remove(matchedIds).catch(() => { });
  196. await addLog(`已关闭 ${matchedIds.length} 个匹配 ${prefix} 的 localhost 残留标签页。`, 'info');
  197. return matchedIds.length;
  198. }
  199. async function pingContentScriptOnTab(tabId) {
  200. if (!Number.isInteger(tabId)) return null;
  201. try {
  202. return await chrome.tabs.sendMessage(tabId, {
  203. type: 'PING',
  204. source: 'background',
  205. payload: {},
  206. });
  207. } catch {
  208. return null;
  209. }
  210. }
  211. async function waitForTabUrlFamily(source, tabId, referenceUrl, options = {}) {
  212. const { timeoutMs = 15000, retryDelayMs = 400 } = options;
  213. const start = Date.now();
  214. while (Date.now() - start < timeoutMs) {
  215. try {
  216. const tab = await chrome.tabs.get(tabId);
  217. if (matchesSourceUrlFamily(source, tab.url, referenceUrl)) {
  218. return tab;
  219. }
  220. } catch {
  221. return null;
  222. }
  223. await sleepOrStop(retryDelayMs);
  224. }
  225. return null;
  226. }
  227. async function waitForTabUrlMatch(tabId, matcher, options = {}) {
  228. const { timeoutMs = 15000, retryDelayMs = 400 } = options;
  229. const start = Date.now();
  230. while (Date.now() - start < timeoutMs) {
  231. try {
  232. const tab = await chrome.tabs.get(tabId);
  233. if (matcher(tab.url || '', tab)) {
  234. return tab;
  235. }
  236. } catch {
  237. return null;
  238. }
  239. await sleepOrStop(retryDelayMs);
  240. }
  241. return null;
  242. }
  243. async function waitForTabComplete(tabId, options = {}) {
  244. const { timeoutMs = 15000, retryDelayMs = 300 } = options;
  245. const start = Date.now();
  246. while (Date.now() - start < timeoutMs) {
  247. try {
  248. const tab = await chrome.tabs.get(tabId);
  249. if (tab?.status === 'complete') {
  250. return tab;
  251. }
  252. } catch {
  253. return null;
  254. }
  255. await sleepOrStop(retryDelayMs);
  256. }
  257. try {
  258. return await chrome.tabs.get(tabId);
  259. } catch {
  260. return null;
  261. }
  262. }
  263. async function waitForTabStableComplete(tabId, options = {}) {
  264. const {
  265. timeoutMs = 30000,
  266. retryDelayMs = 300,
  267. stableMs = 1000,
  268. initialDelayMs = 0,
  269. } = options;
  270. const start = Date.now();
  271. let lastUrl = '';
  272. let lastStatus = '';
  273. let stableStartedAt = 0;
  274. let lastTab = null;
  275. if (initialDelayMs > 0) {
  276. await sleepOrStop(initialDelayMs);
  277. }
  278. while (Date.now() - start < timeoutMs) {
  279. throwIfStopped();
  280. try {
  281. lastTab = await chrome.tabs.get(tabId);
  282. } catch {
  283. return null;
  284. }
  285. const currentUrl = String(lastTab?.url || '');
  286. const currentStatus = String(lastTab?.status || '');
  287. if (currentStatus === 'complete') {
  288. if (currentUrl !== lastUrl || currentStatus !== lastStatus || !stableStartedAt) {
  289. stableStartedAt = Date.now();
  290. }
  291. if (Date.now() - stableStartedAt >= stableMs) {
  292. return lastTab;
  293. }
  294. } else {
  295. stableStartedAt = 0;
  296. }
  297. lastUrl = currentUrl;
  298. lastStatus = currentStatus;
  299. await sleepOrStop(retryDelayMs);
  300. }
  301. return lastTab;
  302. }
  303. async function ensureContentScriptReadyOnTab(source, tabId, options = {}) {
  304. const {
  305. inject = null,
  306. injectSource = null,
  307. timeoutMs = 30000,
  308. retryDelayMs = 700,
  309. logMessage = '',
  310. } = options;
  311. const start = Date.now();
  312. let lastError = null;
  313. let logged = false;
  314. let attempt = 0;
  315. console.log(
  316. LOG_PREFIX,
  317. `[ensureContentScriptReadyOnTab] start ${source} tab=${tabId}, timeout=${timeoutMs}ms, inject=${Array.isArray(inject) ? inject.join(',') : 'none'}`
  318. );
  319. while (Date.now() - start < timeoutMs) {
  320. attempt += 1;
  321. const pong = await pingContentScriptOnTab(tabId);
  322. if (pong?.ok && (!pong.source || pong.source === source)) {
  323. console.log(LOG_PREFIX, `[ensureContentScriptReadyOnTab] ready ${source} tab=${tabId} on attempt ${attempt} after ${Date.now() - start}ms`);
  324. await registerTab(source, tabId);
  325. return;
  326. }
  327. if (!inject || !inject.length) {
  328. throw new Error(`${getSourceLabel(source)} 内容脚本未就绪,且未提供可用的注入文件。`);
  329. }
  330. const registry = await getTabRegistry();
  331. if (registry[source]) {
  332. registry[source].ready = false;
  333. await setState({ tabRegistry: registry });
  334. }
  335. try {
  336. if (injectSource) {
  337. await chrome.scripting.executeScript({
  338. target: { tabId },
  339. func: (injectedSource) => {
  340. window.__MULTIPAGE_SOURCE = injectedSource;
  341. },
  342. args: [injectSource],
  343. });
  344. }
  345. await chrome.scripting.executeScript({
  346. target: { tabId },
  347. files: inject,
  348. });
  349. } catch (err) {
  350. lastError = err;
  351. console.warn(LOG_PREFIX, `[ensureContentScriptReadyOnTab] inject attempt ${attempt} failed for ${source} tab=${tabId}: ${err?.message || err}`);
  352. }
  353. const pongAfterInject = await pingContentScriptOnTab(tabId);
  354. if (pongAfterInject?.ok && (!pongAfterInject.source || pongAfterInject.source === source)) {
  355. console.log(LOG_PREFIX, `[ensureContentScriptReadyOnTab] ready after inject ${source} tab=${tabId} on attempt ${attempt} after ${Date.now() - start}ms`);
  356. await registerTab(source, tabId);
  357. return;
  358. }
  359. if (logMessage && !logged) {
  360. console.warn(LOG_PREFIX, `[ensureContentScriptReadyOnTab] ${source} tab=${tabId} still not ready after ${Date.now() - start}ms`);
  361. await addLog(logMessage, 'warn');
  362. logged = true;
  363. }
  364. await sleepOrStop(retryDelayMs);
  365. }
  366. throw lastError || new Error(`${getSourceLabel(source)} 内容脚本长时间未就绪。`);
  367. }
  368. function getContentScriptResponseTimeoutMs(message) {
  369. if (!message || typeof message !== 'object') return 30000;
  370. if (message.type === 'EXECUTE_STEP' && Number(message.step) === 6) return 75000;
  371. if (message.type === 'POLL_EMAIL') {
  372. const maxAttempts = Math.max(1, Number(message.payload?.maxAttempts) || 1);
  373. const intervalMs = Math.max(0, Number(message.payload?.intervalMs) || 0);
  374. return Math.max(45000, maxAttempts * intervalMs + 25000);
  375. }
  376. if (message.type === 'FILL_CODE') return Number(message.step) === 7 ? 45000 : 30000;
  377. if (message.type === 'PREPARE_SIGNUP_VERIFICATION') return 45000;
  378. return 30000;
  379. }
  380. function getMessageDebugLabel(source, message, tabId = null) {
  381. const parts = [source || 'unknown', message?.type || 'UNKNOWN'];
  382. if (Number.isInteger(message?.step)) parts.push(`step=${message.step}`);
  383. if (Number.isInteger(tabId)) parts.push(`tab=${tabId}`);
  384. return parts.join(' ');
  385. }
  386. function summarizeMessageResultForDebug(result) {
  387. if (result === undefined) return 'undefined';
  388. if (result === null) return 'null';
  389. if (typeof result !== 'object') return JSON.stringify(result);
  390. const summary = {};
  391. for (const key of ['ok', 'error', 'stopped', 'source', 'step']) {
  392. if (key in result) summary[key] = result[key];
  393. }
  394. if (result.payload && typeof result.payload === 'object') {
  395. summary.payloadKeys = Object.keys(result.payload);
  396. }
  397. return JSON.stringify(summary);
  398. }
  399. function sendTabMessageWithTimeout(tabId, source, message, responseTimeoutMs = getContentScriptResponseTimeoutMs(message)) {
  400. return new Promise((resolve, reject) => {
  401. let settled = false;
  402. const startedAt = Date.now();
  403. const debugLabel = getMessageDebugLabel(source, message, tabId);
  404. console.log(LOG_PREFIX, `[sendTabMessageWithTimeout] dispatch ${debugLabel}, timeout=${responseTimeoutMs}ms`);
  405. const timer = setTimeout(() => {
  406. if (settled) return;
  407. settled = true;
  408. const seconds = Math.ceil(responseTimeoutMs / 1000);
  409. console.warn(LOG_PREFIX, `[sendTabMessageWithTimeout] timeout ${debugLabel} after ${Date.now() - startedAt}ms`);
  410. reject(new Error(`Content script on ${source} did not respond in ${seconds}s. Try refreshing the tab and retry.`));
  411. }, responseTimeoutMs);
  412. chrome.tabs.sendMessage(tabId, message)
  413. .then((value) => {
  414. const elapsed = Date.now() - startedAt;
  415. if (settled) return;
  416. settled = true;
  417. clearTimeout(timer);
  418. console.log(LOG_PREFIX, `[sendTabMessageWithTimeout] response ${debugLabel} after ${elapsed}ms: ${summarizeMessageResultForDebug(value)}`);
  419. resolve(value);
  420. })
  421. .catch((error) => {
  422. const elapsed = Date.now() - startedAt;
  423. if (settled) return;
  424. settled = true;
  425. clearTimeout(timer);
  426. console.warn(LOG_PREFIX, `[sendTabMessageWithTimeout] rejection ${debugLabel} after ${elapsed}ms: ${error?.message || error}`);
  427. reject(error);
  428. });
  429. });
  430. }
  431. function queueCommand(source, message, timeout = 15000) {
  432. return new Promise((resolve, reject) => {
  433. const timer = setTimeout(() => {
  434. pendingCommands.delete(source);
  435. reject(new Error(`Content script on ${source} did not respond in ${timeout / 1000}s. Try refreshing the tab and retry.`));
  436. }, timeout);
  437. pendingCommands.set(source, { message, resolve, reject, timer });
  438. console.log(LOG_PREFIX, `Command queued for ${source} (waiting for ready)`);
  439. });
  440. }
  441. function flushCommand(source, tabId) {
  442. const pending = pendingCommands.get(source);
  443. if (pending) {
  444. clearTimeout(pending.timer);
  445. pendingCommands.delete(source);
  446. sendTabMessageWithTimeout(tabId, source, pending.message).then(pending.resolve).catch(pending.reject);
  447. console.log(LOG_PREFIX, `Flushed queued command to ${source} (tab ${tabId})`);
  448. }
  449. }
  450. function cancelPendingCommands(reason = STOP_ERROR_MESSAGE) {
  451. for (const [source, pending] of pendingCommands.entries()) {
  452. clearTimeout(pending.timer);
  453. pending.reject(new Error(reason));
  454. pendingCommands.delete(source);
  455. console.log(LOG_PREFIX, `Cancelled queued command for ${source}`);
  456. }
  457. }
  458. async function reuseOrCreateTab(source, url, options = {}) {
  459. const alive = await isTabAlive(source);
  460. if (alive) {
  461. const tabId = await getTabId(source);
  462. await closeConflictingTabsForSource(source, url, { excludeTabIds: [tabId] });
  463. const currentTab = await chrome.tabs.get(tabId);
  464. const sameUrl = currentTab.url === url;
  465. const shouldReloadOnReuse = sameUrl && options.reloadIfSameUrl;
  466. const registry = await getTabRegistry();
  467. if (sameUrl) {
  468. await chrome.tabs.update(tabId, { active: true });
  469. if (shouldReloadOnReuse) {
  470. if (registry[source]) registry[source].ready = false;
  471. await setState({ tabRegistry: registry });
  472. await chrome.tabs.reload(tabId);
  473. await waitForTabUpdateComplete(tabId);
  474. }
  475. if (options.inject) {
  476. if (registry[source]) registry[source].ready = false;
  477. await setState({ tabRegistry: registry });
  478. if (options.injectSource) {
  479. await chrome.scripting.executeScript({
  480. target: { tabId },
  481. func: (injectedSource) => {
  482. window.__MULTIPAGE_SOURCE = injectedSource;
  483. },
  484. args: [options.injectSource],
  485. });
  486. }
  487. await chrome.scripting.executeScript({
  488. target: { tabId },
  489. files: options.inject,
  490. });
  491. await sleepOrStop(500);
  492. }
  493. await rememberSourceLastUrl(source, url);
  494. return tabId;
  495. }
  496. if (registry[source]) registry[source].ready = false;
  497. await setState({ tabRegistry: registry });
  498. await chrome.tabs.update(tabId, { url, active: true });
  499. await waitForTabUpdateComplete(tabId);
  500. if (options.inject) {
  501. if (options.injectSource) {
  502. await chrome.scripting.executeScript({
  503. target: { tabId },
  504. func: (injectedSource) => {
  505. window.__MULTIPAGE_SOURCE = injectedSource;
  506. },
  507. args: [options.injectSource],
  508. });
  509. }
  510. await chrome.scripting.executeScript({
  511. target: { tabId },
  512. files: options.inject,
  513. });
  514. }
  515. await sleepOrStop(500);
  516. await rememberSourceLastUrl(source, url);
  517. return tabId;
  518. }
  519. const reusableTabs = sortReusableTabsByPriority(
  520. await findReusableTabsForSource(source, url),
  521. url
  522. );
  523. if (reusableTabs.length) {
  524. const [keeper] = reusableTabs;
  525. await registerTab(source, keeper.id);
  526. await closeConflictingTabsForSource(source, url, { excludeTabIds: [keeper.id] });
  527. const currentTab = await chrome.tabs.get(keeper.id);
  528. const sameUrl = currentTab.url === url;
  529. const shouldReloadOnReuse = sameUrl && options.reloadIfSameUrl;
  530. const registry = await getTabRegistry();
  531. if (sameUrl) {
  532. await chrome.tabs.update(keeper.id, { active: true });
  533. if (shouldReloadOnReuse) {
  534. if (registry[source]) registry[source].ready = false;
  535. await setState({ tabRegistry: registry });
  536. await chrome.tabs.reload(keeper.id);
  537. await waitForTabUpdateComplete(keeper.id);
  538. }
  539. if (options.inject) {
  540. if (registry[source]) registry[source].ready = false;
  541. await setState({ tabRegistry: registry });
  542. if (options.injectSource) {
  543. await chrome.scripting.executeScript({
  544. target: { tabId: keeper.id },
  545. func: (injectedSource) => {
  546. window.__MULTIPAGE_SOURCE = injectedSource;
  547. },
  548. args: [options.injectSource],
  549. });
  550. }
  551. await chrome.scripting.executeScript({
  552. target: { tabId: keeper.id },
  553. files: options.inject,
  554. });
  555. await sleepOrStop(500);
  556. }
  557. await rememberSourceLastUrl(source, url);
  558. return keeper.id;
  559. }
  560. if (registry[source]) registry[source].ready = false;
  561. await setState({ tabRegistry: registry });
  562. await chrome.tabs.update(keeper.id, { url, active: true });
  563. await waitForTabUpdateComplete(keeper.id);
  564. if (options.inject) {
  565. if (options.injectSource) {
  566. await chrome.scripting.executeScript({
  567. target: { tabId: keeper.id },
  568. func: (injectedSource) => {
  569. window.__MULTIPAGE_SOURCE = injectedSource;
  570. },
  571. args: [options.injectSource],
  572. });
  573. }
  574. await chrome.scripting.executeScript({
  575. target: { tabId: keeper.id },
  576. files: options.inject,
  577. });
  578. }
  579. await sleepOrStop(500);
  580. await rememberSourceLastUrl(source, url);
  581. return keeper.id;
  582. }
  583. await closeConflictingTabsForSource(source, url);
  584. const tab = await chrome.tabs.create({ url, active: true });
  585. if (options.inject) {
  586. await waitForTabUpdateComplete(tab.id);
  587. if (options.injectSource) {
  588. await chrome.scripting.executeScript({
  589. target: { tabId: tab.id },
  590. func: (injectedSource) => {
  591. window.__MULTIPAGE_SOURCE = injectedSource;
  592. },
  593. args: [options.injectSource],
  594. });
  595. }
  596. await chrome.scripting.executeScript({
  597. target: { tabId: tab.id },
  598. files: options.inject,
  599. });
  600. }
  601. await rememberSourceLastUrl(source, url);
  602. return tab.id;
  603. }
  604. async function sendToContentScript(source, message, options = {}) {
  605. throwIfStopped();
  606. const { responseTimeoutMs = getContentScriptResponseTimeoutMs(message) } = options;
  607. const registry = await getTabRegistry();
  608. const entry = registry[source];
  609. if (!entry || !entry.ready) {
  610. throwIfStopped();
  611. return queueCommand(source, message);
  612. }
  613. const alive = await isTabAlive(source);
  614. throwIfStopped();
  615. if (!alive) {
  616. return queueCommand(source, message);
  617. }
  618. throwIfStopped();
  619. return sendTabMessageWithTimeout(entry.tabId, source, message, responseTimeoutMs);
  620. }
  621. async function sendToContentScriptResilient(source, message, options = {}) {
  622. const {
  623. timeoutMs = 30000,
  624. retryDelayMs = 600,
  625. logMessage = '',
  626. responseTimeoutMs,
  627. } = options;
  628. const start = Date.now();
  629. let lastError = null;
  630. let logged = false;
  631. let attempt = 0;
  632. while (Date.now() - start < timeoutMs) {
  633. throwIfStopped();
  634. attempt += 1;
  635. try {
  636. return await sendToContentScript(
  637. source,
  638. message,
  639. responseTimeoutMs !== undefined ? { responseTimeoutMs } : {}
  640. );
  641. } catch (err) {
  642. const retryable = isRetryableContentScriptTransportError(err);
  643. if (!retryable) {
  644. throw err;
  645. }
  646. lastError = err;
  647. if (logMessage && !logged) {
  648. await addLog(logMessage, 'warn');
  649. logged = true;
  650. }
  651. await sleepOrStop(retryDelayMs);
  652. }
  653. }
  654. throw lastError || new Error(`等待 ${getSourceLabel(source)} 重新就绪超时。`);
  655. }
  656. async function sendToMailContentScriptResilient(mail, message, options = {}) {
  657. const {
  658. timeoutMs = 45000,
  659. maxRecoveryAttempts = 2,
  660. responseTimeoutMs,
  661. } = options;
  662. const start = Date.now();
  663. let lastError = null;
  664. let recoveries = 0;
  665. let logged = false;
  666. while (Date.now() - start < timeoutMs) {
  667. throwIfStopped();
  668. try {
  669. return await sendToContentScript(
  670. mail.source,
  671. message,
  672. responseTimeoutMs !== undefined ? { responseTimeoutMs } : {}
  673. );
  674. } catch (err) {
  675. if (!isRetryableContentScriptTransportError(err)) {
  676. throw err;
  677. }
  678. lastError = err;
  679. if (!logged) {
  680. await addLog(`步骤 ${message.step}:${mail.label} 页面通信异常,正在尝试让邮箱页重新就绪...`, 'warn');
  681. logged = true;
  682. }
  683. if (recoveries >= maxRecoveryAttempts) {
  684. break;
  685. }
  686. recoveries += 1;
  687. await reuseOrCreateTab(mail.source, mail.url, {
  688. inject: mail.inject,
  689. injectSource: mail.injectSource,
  690. reloadIfSameUrl: true,
  691. });
  692. await sleepOrStop(800);
  693. }
  694. }
  695. throw lastError || new Error(`${mail.label} 页面未能重新就绪。`);
  696. }
  697. return {
  698. buildLocalhostCleanupPrefix,
  699. cancelPendingCommands,
  700. closeConflictingTabsForSource,
  701. closeLocalhostCallbackTabs,
  702. closeTabsByUrlPrefix,
  703. ensureContentScriptReadyOnTab,
  704. flushCommand,
  705. getContentScriptResponseTimeoutMs,
  706. getMessageDebugLabel,
  707. getTabId,
  708. getTabRegistry,
  709. isLocalhostOAuthCallbackTabMatch,
  710. isTabAlive,
  711. pingContentScriptOnTab,
  712. queueCommand,
  713. registerTab,
  714. rememberSourceLastUrl,
  715. reuseOrCreateTab,
  716. sendTabMessageWithTimeout,
  717. sendToContentScript,
  718. sendToContentScriptResilient,
  719. sendToMailContentScriptResilient,
  720. summarizeMessageResultForDebug,
  721. waitForTabComplete,
  722. waitForTabStableComplete,
  723. waitForTabUrlFamily,
  724. waitForTabUrlMatch,
  725. };
  726. }
  727. return {
  728. createTabRuntime,
  729. };
  730. });