auth-page-recovery.js 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182
  1. (function authPageRecoveryModule(root, factory) {
  2. if (typeof module !== 'undefined' && module.exports) {
  3. module.exports = factory();
  4. return;
  5. }
  6. root.MultiPageAuthPageRecovery = factory();
  7. })(typeof self !== 'undefined' ? self : globalThis, function createAuthPageRecoveryModule() {
  8. function createAuthPageRecovery(deps = {}) {
  9. const {
  10. detailPattern = null,
  11. getActionText,
  12. getPageTextSnapshot,
  13. humanPause,
  14. isActionEnabled,
  15. isVisibleElement,
  16. log,
  17. simulateClick,
  18. sleep,
  19. throwIfStopped,
  20. titlePattern = null,
  21. } = deps;
  22. function matchesPathPatterns(pathname, pathPatterns = []) {
  23. if (!Array.isArray(pathPatterns) || !pathPatterns.length) {
  24. return true;
  25. }
  26. return pathPatterns.some((pattern) => pattern instanceof RegExp && pattern.test(pathname));
  27. }
  28. function getAuthRetryButton(options = {}) {
  29. const { allowDisabled = false } = options;
  30. const direct = document.querySelector('button[data-dd-action-name="Try again"]');
  31. if (direct && isVisibleElement(direct) && (allowDisabled || isActionEnabled(direct))) {
  32. return direct;
  33. }
  34. const candidates = document.querySelectorAll('button, [role="button"]');
  35. return Array.from(candidates).find((element) => {
  36. if (!isVisibleElement(element) || (!allowDisabled && !isActionEnabled(element))) {
  37. return false;
  38. }
  39. const text = typeof getActionText === 'function' ? getActionText(element) : '';
  40. return /重试|try\s+again/i.test(text);
  41. }) || null;
  42. }
  43. function getAuthTimeoutErrorPageState(options = {}) {
  44. const { pathPatterns = [] } = options;
  45. const pathname = location.pathname || '';
  46. if (!matchesPathPatterns(pathname, pathPatterns)) {
  47. return null;
  48. }
  49. const retryButton = getAuthRetryButton({ allowDisabled: true });
  50. if (!retryButton) {
  51. return null;
  52. }
  53. const text = typeof getPageTextSnapshot === 'function' ? getPageTextSnapshot() : '';
  54. const title = typeof document !== 'undefined' ? String(document.title || '') : '';
  55. const titleMatched = titlePattern instanceof RegExp
  56. ? titlePattern.test(text) || titlePattern.test(title)
  57. : false;
  58. const detailMatched = detailPattern instanceof RegExp
  59. ? detailPattern.test(text)
  60. : false;
  61. if (!titleMatched && !detailMatched) {
  62. return null;
  63. }
  64. return {
  65. path: pathname,
  66. url: location.href,
  67. retryButton,
  68. retryEnabled: isActionEnabled(retryButton),
  69. titleMatched,
  70. detailMatched,
  71. };
  72. }
  73. async function waitForRetryPageRecoveryAfterClick(options = {}) {
  74. const {
  75. pathPatterns = [],
  76. pollIntervalMs = 250,
  77. settleAfterClickMs = 3000,
  78. } = options;
  79. const startedAt = Date.now();
  80. while (Date.now() - startedAt < settleAfterClickMs) {
  81. if (typeof throwIfStopped === 'function') {
  82. throwIfStopped();
  83. }
  84. const retryState = getAuthTimeoutErrorPageState({ pathPatterns });
  85. if (!retryState) {
  86. return {
  87. recovered: true,
  88. elapsedMs: Date.now() - startedAt,
  89. };
  90. }
  91. await sleep(pollIntervalMs);
  92. }
  93. return {
  94. recovered: false,
  95. elapsedMs: Date.now() - startedAt,
  96. };
  97. }
  98. async function recoverAuthRetryPage(options = {}) {
  99. const {
  100. logLabel = '',
  101. pathPatterns = [],
  102. pollIntervalMs = 250,
  103. step = null,
  104. timeoutMs = 12000,
  105. waitAfterClickMs = 3000,
  106. } = options;
  107. const start = Date.now();
  108. let clickCount = 0;
  109. while (Date.now() - start < timeoutMs) {
  110. if (typeof throwIfStopped === 'function') {
  111. throwIfStopped();
  112. }
  113. const retryState = getAuthTimeoutErrorPageState({ pathPatterns });
  114. if (!retryState) {
  115. return {
  116. recovered: clickCount > 0,
  117. clickCount,
  118. url: location.href,
  119. };
  120. }
  121. if (retryState.retryButton && retryState.retryEnabled) {
  122. clickCount += 1;
  123. if (typeof log === 'function') {
  124. const prefix = logLabel || `步骤 ${step || '?'}:检测到重试页,正在点击“重试”恢复`;
  125. log(`${prefix}(第 ${clickCount} 次)...`, 'warn');
  126. }
  127. if (typeof humanPause === 'function') {
  128. await humanPause(300, 800);
  129. }
  130. simulateClick(retryState.retryButton);
  131. const recoveryResult = await waitForRetryPageRecoveryAfterClick({
  132. pathPatterns,
  133. pollIntervalMs,
  134. settleAfterClickMs: waitAfterClickMs,
  135. });
  136. if (recoveryResult.recovered) {
  137. return {
  138. recovered: true,
  139. clickCount,
  140. url: location.href,
  141. };
  142. }
  143. continue;
  144. }
  145. await sleep(pollIntervalMs);
  146. }
  147. throw new Error(
  148. `${logLabel || `步骤 ${step || '?'}:重试页恢复`}超时。URL: ${location.href}`
  149. );
  150. }
  151. return {
  152. getAuthRetryButton,
  153. getAuthTimeoutErrorPageState,
  154. recoverAuthRetryPage,
  155. };
  156. }
  157. return {
  158. createAuthPageRecovery,
  159. };
  160. });