tab-runtime.js 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798
  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 ensureContentScriptReadyOnTab(source, tabId, options = {}) {
  264. const {
  265. inject = null,
  266. injectSource = null,
  267. timeoutMs = 30000,
  268. retryDelayMs = 700,
  269. logMessage = '',
  270. } = options;
  271. const start = Date.now();
  272. let lastError = null;
  273. let logged = false;
  274. let attempt = 0;
  275. console.log(
  276. LOG_PREFIX,
  277. `[ensureContentScriptReadyOnTab] start ${source} tab=${tabId}, timeout=${timeoutMs}ms, inject=${Array.isArray(inject) ? inject.join(',') : 'none'}`
  278. );
  279. while (Date.now() - start < timeoutMs) {
  280. attempt += 1;
  281. const pong = await pingContentScriptOnTab(tabId);
  282. if (pong?.ok && (!pong.source || pong.source === source)) {
  283. console.log(LOG_PREFIX, `[ensureContentScriptReadyOnTab] ready ${source} tab=${tabId} on attempt ${attempt} after ${Date.now() - start}ms`);
  284. await registerTab(source, tabId);
  285. return;
  286. }
  287. if (!inject || !inject.length) {
  288. throw new Error(`${getSourceLabel(source)} 内容脚本未就绪,且未提供可用的注入文件。`);
  289. }
  290. const registry = await getTabRegistry();
  291. if (registry[source]) {
  292. registry[source].ready = false;
  293. await setState({ tabRegistry: registry });
  294. }
  295. try {
  296. if (injectSource) {
  297. await chrome.scripting.executeScript({
  298. target: { tabId },
  299. func: (injectedSource) => {
  300. window.__MULTIPAGE_SOURCE = injectedSource;
  301. },
  302. args: [injectSource],
  303. });
  304. }
  305. await chrome.scripting.executeScript({
  306. target: { tabId },
  307. files: inject,
  308. });
  309. } catch (err) {
  310. lastError = err;
  311. console.warn(LOG_PREFIX, `[ensureContentScriptReadyOnTab] inject attempt ${attempt} failed for ${source} tab=${tabId}: ${err?.message || err}`);
  312. }
  313. const pongAfterInject = await pingContentScriptOnTab(tabId);
  314. if (pongAfterInject?.ok && (!pongAfterInject.source || pongAfterInject.source === source)) {
  315. console.log(LOG_PREFIX, `[ensureContentScriptReadyOnTab] ready after inject ${source} tab=${tabId} on attempt ${attempt} after ${Date.now() - start}ms`);
  316. await registerTab(source, tabId);
  317. return;
  318. }
  319. if (logMessage && !logged) {
  320. console.warn(LOG_PREFIX, `[ensureContentScriptReadyOnTab] ${source} tab=${tabId} still not ready after ${Date.now() - start}ms`);
  321. await addLog(logMessage, 'warn');
  322. logged = true;
  323. }
  324. await sleepOrStop(retryDelayMs);
  325. }
  326. throw lastError || new Error(`${getSourceLabel(source)} 内容脚本长时间未就绪。`);
  327. }
  328. function getContentScriptResponseTimeoutMs(message) {
  329. if (!message || typeof message !== 'object') return 30000;
  330. if (message.type === 'EXECUTE_STEP' && Number(message.step) === 6) return 75000;
  331. if (message.type === 'POLL_EMAIL') {
  332. const maxAttempts = Math.max(1, Number(message.payload?.maxAttempts) || 1);
  333. const intervalMs = Math.max(0, Number(message.payload?.intervalMs) || 0);
  334. return Math.max(45000, maxAttempts * intervalMs + 25000);
  335. }
  336. if (message.type === 'FILL_CODE') return Number(message.step) === 7 ? 45000 : 30000;
  337. if (message.type === 'PREPARE_SIGNUP_VERIFICATION') return 45000;
  338. return 30000;
  339. }
  340. function getMessageDebugLabel(source, message, tabId = null) {
  341. const parts = [source || 'unknown', message?.type || 'UNKNOWN'];
  342. if (Number.isInteger(message?.step)) parts.push(`step=${message.step}`);
  343. if (Number.isInteger(tabId)) parts.push(`tab=${tabId}`);
  344. return parts.join(' ');
  345. }
  346. function summarizeMessageResultForDebug(result) {
  347. if (result === undefined) return 'undefined';
  348. if (result === null) return 'null';
  349. if (typeof result !== 'object') return JSON.stringify(result);
  350. const summary = {};
  351. for (const key of ['ok', 'error', 'stopped', 'source', 'step']) {
  352. if (key in result) summary[key] = result[key];
  353. }
  354. if (result.payload && typeof result.payload === 'object') {
  355. summary.payloadKeys = Object.keys(result.payload);
  356. }
  357. return JSON.stringify(summary);
  358. }
  359. function sendTabMessageWithTimeout(tabId, source, message, responseTimeoutMs = getContentScriptResponseTimeoutMs(message)) {
  360. return new Promise((resolve, reject) => {
  361. let settled = false;
  362. const startedAt = Date.now();
  363. const debugLabel = getMessageDebugLabel(source, message, tabId);
  364. console.log(LOG_PREFIX, `[sendTabMessageWithTimeout] dispatch ${debugLabel}, timeout=${responseTimeoutMs}ms`);
  365. const timer = setTimeout(() => {
  366. if (settled) return;
  367. settled = true;
  368. const seconds = Math.ceil(responseTimeoutMs / 1000);
  369. console.warn(LOG_PREFIX, `[sendTabMessageWithTimeout] timeout ${debugLabel} after ${Date.now() - startedAt}ms`);
  370. reject(new Error(`Content script on ${source} did not respond in ${seconds}s. Try refreshing the tab and retry.`));
  371. }, responseTimeoutMs);
  372. chrome.tabs.sendMessage(tabId, message)
  373. .then((value) => {
  374. const elapsed = Date.now() - startedAt;
  375. if (settled) return;
  376. settled = true;
  377. clearTimeout(timer);
  378. console.log(LOG_PREFIX, `[sendTabMessageWithTimeout] response ${debugLabel} after ${elapsed}ms: ${summarizeMessageResultForDebug(value)}`);
  379. resolve(value);
  380. })
  381. .catch((error) => {
  382. const elapsed = Date.now() - startedAt;
  383. if (settled) return;
  384. settled = true;
  385. clearTimeout(timer);
  386. console.warn(LOG_PREFIX, `[sendTabMessageWithTimeout] rejection ${debugLabel} after ${elapsed}ms: ${error?.message || error}`);
  387. reject(error);
  388. });
  389. });
  390. }
  391. function queueCommand(source, message, timeout = 15000) {
  392. return new Promise((resolve, reject) => {
  393. const timer = setTimeout(() => {
  394. pendingCommands.delete(source);
  395. reject(new Error(`Content script on ${source} did not respond in ${timeout / 1000}s. Try refreshing the tab and retry.`));
  396. }, timeout);
  397. pendingCommands.set(source, { message, resolve, reject, timer });
  398. console.log(LOG_PREFIX, `Command queued for ${source} (waiting for ready)`);
  399. });
  400. }
  401. function flushCommand(source, tabId) {
  402. const pending = pendingCommands.get(source);
  403. if (pending) {
  404. clearTimeout(pending.timer);
  405. pendingCommands.delete(source);
  406. sendTabMessageWithTimeout(tabId, source, pending.message).then(pending.resolve).catch(pending.reject);
  407. console.log(LOG_PREFIX, `Flushed queued command to ${source} (tab ${tabId})`);
  408. }
  409. }
  410. function cancelPendingCommands(reason = STOP_ERROR_MESSAGE) {
  411. for (const [source, pending] of pendingCommands.entries()) {
  412. clearTimeout(pending.timer);
  413. pending.reject(new Error(reason));
  414. pendingCommands.delete(source);
  415. console.log(LOG_PREFIX, `Cancelled queued command for ${source}`);
  416. }
  417. }
  418. async function reuseOrCreateTab(source, url, options = {}) {
  419. const alive = await isTabAlive(source);
  420. if (alive) {
  421. const tabId = await getTabId(source);
  422. await closeConflictingTabsForSource(source, url, { excludeTabIds: [tabId] });
  423. const currentTab = await chrome.tabs.get(tabId);
  424. const sameUrl = currentTab.url === url;
  425. const shouldReloadOnReuse = sameUrl && options.reloadIfSameUrl;
  426. const registry = await getTabRegistry();
  427. if (sameUrl) {
  428. await chrome.tabs.update(tabId, { active: true });
  429. if (shouldReloadOnReuse) {
  430. if (registry[source]) registry[source].ready = false;
  431. await setState({ tabRegistry: registry });
  432. await chrome.tabs.reload(tabId);
  433. await waitForTabUpdateComplete(tabId);
  434. }
  435. if (options.inject) {
  436. if (registry[source]) registry[source].ready = false;
  437. await setState({ tabRegistry: registry });
  438. if (options.injectSource) {
  439. await chrome.scripting.executeScript({
  440. target: { tabId },
  441. func: (injectedSource) => {
  442. window.__MULTIPAGE_SOURCE = injectedSource;
  443. },
  444. args: [options.injectSource],
  445. });
  446. }
  447. await chrome.scripting.executeScript({
  448. target: { tabId },
  449. files: options.inject,
  450. });
  451. await sleepOrStop(500);
  452. }
  453. await rememberSourceLastUrl(source, url);
  454. return tabId;
  455. }
  456. if (registry[source]) registry[source].ready = false;
  457. await setState({ tabRegistry: registry });
  458. await chrome.tabs.update(tabId, { url, active: true });
  459. await waitForTabUpdateComplete(tabId);
  460. if (options.inject) {
  461. if (options.injectSource) {
  462. await chrome.scripting.executeScript({
  463. target: { tabId },
  464. func: (injectedSource) => {
  465. window.__MULTIPAGE_SOURCE = injectedSource;
  466. },
  467. args: [options.injectSource],
  468. });
  469. }
  470. await chrome.scripting.executeScript({
  471. target: { tabId },
  472. files: options.inject,
  473. });
  474. }
  475. await sleepOrStop(500);
  476. await rememberSourceLastUrl(source, url);
  477. return tabId;
  478. }
  479. const reusableTabs = sortReusableTabsByPriority(
  480. await findReusableTabsForSource(source, url),
  481. url
  482. );
  483. if (reusableTabs.length) {
  484. const [keeper] = reusableTabs;
  485. await registerTab(source, keeper.id);
  486. await closeConflictingTabsForSource(source, url, { excludeTabIds: [keeper.id] });
  487. const currentTab = await chrome.tabs.get(keeper.id);
  488. const sameUrl = currentTab.url === url;
  489. const shouldReloadOnReuse = sameUrl && options.reloadIfSameUrl;
  490. const registry = await getTabRegistry();
  491. if (sameUrl) {
  492. await chrome.tabs.update(keeper.id, { active: true });
  493. if (shouldReloadOnReuse) {
  494. if (registry[source]) registry[source].ready = false;
  495. await setState({ tabRegistry: registry });
  496. await chrome.tabs.reload(keeper.id);
  497. await waitForTabUpdateComplete(keeper.id);
  498. }
  499. if (options.inject) {
  500. if (registry[source]) registry[source].ready = false;
  501. await setState({ tabRegistry: registry });
  502. if (options.injectSource) {
  503. await chrome.scripting.executeScript({
  504. target: { tabId: keeper.id },
  505. func: (injectedSource) => {
  506. window.__MULTIPAGE_SOURCE = injectedSource;
  507. },
  508. args: [options.injectSource],
  509. });
  510. }
  511. await chrome.scripting.executeScript({
  512. target: { tabId: keeper.id },
  513. files: options.inject,
  514. });
  515. await sleepOrStop(500);
  516. }
  517. await rememberSourceLastUrl(source, url);
  518. return keeper.id;
  519. }
  520. if (registry[source]) registry[source].ready = false;
  521. await setState({ tabRegistry: registry });
  522. await chrome.tabs.update(keeper.id, { url, active: true });
  523. await waitForTabUpdateComplete(keeper.id);
  524. if (options.inject) {
  525. if (options.injectSource) {
  526. await chrome.scripting.executeScript({
  527. target: { tabId: keeper.id },
  528. func: (injectedSource) => {
  529. window.__MULTIPAGE_SOURCE = injectedSource;
  530. },
  531. args: [options.injectSource],
  532. });
  533. }
  534. await chrome.scripting.executeScript({
  535. target: { tabId: keeper.id },
  536. files: options.inject,
  537. });
  538. }
  539. await sleepOrStop(500);
  540. await rememberSourceLastUrl(source, url);
  541. return keeper.id;
  542. }
  543. await closeConflictingTabsForSource(source, url);
  544. const tab = await chrome.tabs.create({ url, active: true });
  545. if (options.inject) {
  546. await waitForTabUpdateComplete(tab.id);
  547. if (options.injectSource) {
  548. await chrome.scripting.executeScript({
  549. target: { tabId: tab.id },
  550. func: (injectedSource) => {
  551. window.__MULTIPAGE_SOURCE = injectedSource;
  552. },
  553. args: [options.injectSource],
  554. });
  555. }
  556. await chrome.scripting.executeScript({
  557. target: { tabId: tab.id },
  558. files: options.inject,
  559. });
  560. }
  561. await rememberSourceLastUrl(source, url);
  562. return tab.id;
  563. }
  564. async function sendToContentScript(source, message, options = {}) {
  565. throwIfStopped();
  566. const { responseTimeoutMs = getContentScriptResponseTimeoutMs(message) } = options;
  567. const registry = await getTabRegistry();
  568. const entry = registry[source];
  569. if (!entry || !entry.ready) {
  570. throwIfStopped();
  571. return queueCommand(source, message);
  572. }
  573. const alive = await isTabAlive(source);
  574. throwIfStopped();
  575. if (!alive) {
  576. return queueCommand(source, message);
  577. }
  578. throwIfStopped();
  579. return sendTabMessageWithTimeout(entry.tabId, source, message, responseTimeoutMs);
  580. }
  581. async function sendToContentScriptResilient(source, message, options = {}) {
  582. const {
  583. timeoutMs = 30000,
  584. retryDelayMs = 600,
  585. logMessage = '',
  586. responseTimeoutMs,
  587. } = options;
  588. const start = Date.now();
  589. let lastError = null;
  590. let logged = false;
  591. let attempt = 0;
  592. while (Date.now() - start < timeoutMs) {
  593. throwIfStopped();
  594. attempt += 1;
  595. try {
  596. return await sendToContentScript(
  597. source,
  598. message,
  599. responseTimeoutMs !== undefined ? { responseTimeoutMs } : {}
  600. );
  601. } catch (err) {
  602. const retryable = isRetryableContentScriptTransportError(err);
  603. if (!retryable) {
  604. throw err;
  605. }
  606. lastError = err;
  607. if (logMessage && !logged) {
  608. await addLog(logMessage, 'warn');
  609. logged = true;
  610. }
  611. await sleepOrStop(retryDelayMs);
  612. }
  613. }
  614. throw lastError || new Error(`等待 ${getSourceLabel(source)} 重新就绪超时。`);
  615. }
  616. async function sendToMailContentScriptResilient(mail, message, options = {}) {
  617. const {
  618. timeoutMs = 45000,
  619. maxRecoveryAttempts = 2,
  620. responseTimeoutMs,
  621. } = options;
  622. const start = Date.now();
  623. let lastError = null;
  624. let recoveries = 0;
  625. let logged = false;
  626. while (Date.now() - start < timeoutMs) {
  627. throwIfStopped();
  628. try {
  629. return await sendToContentScript(
  630. mail.source,
  631. message,
  632. responseTimeoutMs !== undefined ? { responseTimeoutMs } : {}
  633. );
  634. } catch (err) {
  635. if (!isRetryableContentScriptTransportError(err)) {
  636. throw err;
  637. }
  638. lastError = err;
  639. if (!logged) {
  640. await addLog(`步骤 ${message.step}:${mail.label} 页面通信异常,正在尝试让邮箱页重新就绪...`, 'warn');
  641. logged = true;
  642. }
  643. if (recoveries >= maxRecoveryAttempts) {
  644. break;
  645. }
  646. recoveries += 1;
  647. await reuseOrCreateTab(mail.source, mail.url, {
  648. inject: mail.inject,
  649. injectSource: mail.injectSource,
  650. reloadIfSameUrl: true,
  651. });
  652. await sleepOrStop(800);
  653. }
  654. }
  655. throw lastError || new Error(`${mail.label} 页面未能重新就绪。`);
  656. }
  657. return {
  658. buildLocalhostCleanupPrefix,
  659. cancelPendingCommands,
  660. closeConflictingTabsForSource,
  661. closeLocalhostCallbackTabs,
  662. closeTabsByUrlPrefix,
  663. ensureContentScriptReadyOnTab,
  664. flushCommand,
  665. getContentScriptResponseTimeoutMs,
  666. getMessageDebugLabel,
  667. getTabId,
  668. getTabRegistry,
  669. isLocalhostOAuthCallbackTabMatch,
  670. isTabAlive,
  671. pingContentScriptOnTab,
  672. queueCommand,
  673. registerTab,
  674. rememberSourceLastUrl,
  675. reuseOrCreateTab,
  676. sendTabMessageWithTimeout,
  677. sendToContentScript,
  678. sendToContentScriptResilient,
  679. sendToMailContentScriptResilient,
  680. summarizeMessageResultForDebug,
  681. waitForTabComplete,
  682. waitForTabUrlFamily,
  683. waitForTabUrlMatch,
  684. };
  685. }
  686. return {
  687. createTabRuntime,
  688. };
  689. });