verification-flow.js 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709
  1. (function attachBackgroundVerificationFlow(root, factory) {
  2. root.MultiPageBackgroundVerificationFlow = factory();
  3. })(typeof self !== 'undefined' ? self : globalThis, function createBackgroundVerificationFlowModule() {
  4. function createVerificationFlowHelpers(deps = {}) {
  5. const {
  6. A4SKY_PROVIDER,
  7. addLog,
  8. chrome,
  9. CLOUDFLARE_TEMP_EMAIL_PROVIDER,
  10. completeStepFromBackground,
  11. confirmCustomVerificationStepBypassRequest,
  12. getHotmailVerificationPollConfig,
  13. getHotmailVerificationRequestTimestamp,
  14. getState,
  15. getTabId,
  16. HOTMAIL_PROVIDER,
  17. isStopError,
  18. LUCKMAIL_PROVIDER,
  19. MAIL_2925_VERIFICATION_INTERVAL_MS,
  20. MAIL_2925_VERIFICATION_MAX_ATTEMPTS,
  21. pollA4skyImapVerificationCode,
  22. pollCloudflareTempEmailVerificationCode,
  23. pollHotmailVerificationCode,
  24. pollLuckmailVerificationCode,
  25. sendToContentScript,
  26. sendToMailContentScriptResilient,
  27. setState,
  28. sleepWithStop,
  29. throwIfStopped,
  30. VERIFICATION_POLL_MAX_ROUNDS,
  31. } = deps;
  32. const A4SKY_MANUAL_LOGIN_RESPONSE_TIMEOUT_MS = 20 * 60 * 1000;
  33. function getVerificationCodeStateKey(step) {
  34. return step === 4 ? 'lastSignupCode' : 'lastLoginCode';
  35. }
  36. function getVerificationCodeLabel(step) {
  37. return step === 4 ? '注册' : '登录';
  38. }
  39. function getVerificationResendStateKey() {
  40. return 'verificationResendCount';
  41. }
  42. function normalizeVerificationResendCount(value, fallback = 0) {
  43. const numeric = Number(value);
  44. if (!Number.isFinite(numeric)) {
  45. return Math.max(0, Math.floor(Number(fallback) || 0));
  46. }
  47. return Math.min(20, Math.max(0, Math.floor(numeric)));
  48. }
  49. function getLegacyVerificationResendCountDefault(step, options = {}) {
  50. const requestFreshCodeFirst = Boolean(options.requestFreshCodeFirst);
  51. const legacyMaxRounds = Math.max(1, Math.floor(Number(VERIFICATION_POLL_MAX_ROUNDS) || 1));
  52. if (step === 4 && requestFreshCodeFirst) {
  53. return legacyMaxRounds;
  54. }
  55. return Math.max(0, legacyMaxRounds - 1);
  56. }
  57. function getConfiguredVerificationResendCount(step, state, options = {}) {
  58. const stateKey = getVerificationResendStateKey(step);
  59. const configuredValue = state?.[stateKey] !== undefined
  60. ? state[stateKey]
  61. : (state?.signupVerificationResendCount ?? state?.loginVerificationResendCount);
  62. return normalizeVerificationResendCount(
  63. configuredValue,
  64. getLegacyVerificationResendCountDefault(step, options)
  65. );
  66. }
  67. function resolveMaxResendRequests(pollOverrides = {}) {
  68. if (pollOverrides.maxResendRequests !== undefined) {
  69. return normalizeVerificationResendCount(pollOverrides.maxResendRequests, 0);
  70. }
  71. const legacyMaxRounds = Number(pollOverrides.maxRounds);
  72. if (Number.isFinite(legacyMaxRounds)) {
  73. return Math.max(0, Math.floor(legacyMaxRounds) - 1);
  74. }
  75. return Math.max(0, Math.floor(Number(VERIFICATION_POLL_MAX_ROUNDS) || 1) - 1);
  76. }
  77. async function confirmCustomVerificationStepBypass(step) {
  78. const verificationLabel = getVerificationCodeLabel(step);
  79. await addLog(`步骤 ${step}:当前为自定义邮箱模式,请手动在页面中输入${verificationLabel}验证码并进入下一页面。`, 'warn');
  80. let response = null;
  81. try {
  82. response = await confirmCustomVerificationStepBypassRequest(step);
  83. } catch {
  84. throw new Error(`步骤 ${step}:无法打开确认弹窗,请先保持侧边栏打开后重试。`);
  85. }
  86. if (response?.error) {
  87. throw new Error(response.error);
  88. }
  89. if (!response?.confirmed) {
  90. throw new Error(`步骤 ${step}:已取消手动${verificationLabel}验证码确认。`);
  91. }
  92. await setState({
  93. lastEmailTimestamp: null,
  94. signupVerificationRequestedAt: null,
  95. loginVerificationRequestedAt: null,
  96. });
  97. await deps.setStepStatus(step, 'skipped');
  98. await addLog(`步骤 ${step}:已确认手动完成${verificationLabel}验证码输入,当前步骤已跳过。`, 'warn');
  99. }
  100. function getVerificationPollPayload(step, state, overrides = {}) {
  101. const is2925Provider = state?.mailProvider === '2925';
  102. if (step === 4) {
  103. return {
  104. filterAfterTimestamp: getHotmailVerificationRequestTimestamp(4, state),
  105. senderFilters: ['openai', 'noreply', 'verify', 'auth', 'duckduckgo', 'forward'],
  106. subjectFilters: ['verify', 'verification', 'code', '验证码', 'confirm'],
  107. targetEmail: state.email,
  108. maxAttempts: is2925Provider ? MAIL_2925_VERIFICATION_MAX_ATTEMPTS : 5,
  109. intervalMs: is2925Provider ? MAIL_2925_VERIFICATION_INTERVAL_MS : 3000,
  110. ...overrides,
  111. };
  112. }
  113. return {
  114. filterAfterTimestamp: getHotmailVerificationRequestTimestamp(8, state),
  115. senderFilters: ['openai', 'noreply', 'verify', 'auth', 'chatgpt', 'duckduckgo', 'forward'],
  116. subjectFilters: ['verify', 'verification', 'code', '验证码', 'confirm', 'login'],
  117. targetEmail: state.email,
  118. maxAttempts: is2925Provider ? MAIL_2925_VERIFICATION_MAX_ATTEMPTS : 5,
  119. intervalMs: is2925Provider ? MAIL_2925_VERIFICATION_INTERVAL_MS : 3000,
  120. ...overrides,
  121. };
  122. }
  123. async function getRemainingTimeBudgetMs(step, options = {}, actionLabel = '') {
  124. const resolver = typeof options.getRemainingTimeMs === 'function'
  125. ? options.getRemainingTimeMs
  126. : null;
  127. if (!resolver) {
  128. return null;
  129. }
  130. const value = await resolver({ step, actionLabel });
  131. const numeric = Number(value);
  132. if (!Number.isFinite(numeric)) {
  133. return null;
  134. }
  135. return Math.max(0, Math.floor(numeric));
  136. }
  137. async function getResponseTimeoutMsForStep(step, options = {}, fallbackMs = 30000, actionLabel = '') {
  138. const remainingMs = await getRemainingTimeBudgetMs(step, options, actionLabel);
  139. if (remainingMs === null) {
  140. return Math.max(1000, Number(fallbackMs) || 1000);
  141. }
  142. return Math.max(1000, Math.min(Math.max(1000, Number(fallbackMs) || 1000), remainingMs));
  143. }
  144. async function applyMailPollingTimeBudget(step, payload, options = {}, actionLabel = '') {
  145. const nextPayload = { ...payload };
  146. const intervalMs = Math.max(1, Number(nextPayload.intervalMs) || 3000);
  147. const baseMaxAttempts = Math.max(1, Number(nextPayload.maxAttempts) || 1);
  148. const remainingMs = await getRemainingTimeBudgetMs(step, options, actionLabel);
  149. if (remainingMs !== null) {
  150. nextPayload.maxAttempts = Math.max(
  151. 1,
  152. Math.min(baseMaxAttempts, Math.floor(Math.max(0, remainingMs - 1000) / intervalMs) + 1)
  153. );
  154. }
  155. const defaultResponseTimeoutMs = Math.max(45000, nextPayload.maxAttempts * intervalMs + 25000);
  156. const responseTimeoutMs = remainingMs === null
  157. ? defaultResponseTimeoutMs
  158. : Math.max(1000, Math.min(defaultResponseTimeoutMs, remainingMs));
  159. return {
  160. payload: nextPayload,
  161. responseTimeoutMs,
  162. timeoutMs: responseTimeoutMs,
  163. };
  164. }
  165. function getMailContentScriptTimeoutOptions(mail, timedPoll = {}) {
  166. if (mail?.provider === A4SKY_PROVIDER) {
  167. const extendedTimeoutMs = Math.max(
  168. Number(timedPoll.responseTimeoutMs) || 0,
  169. A4SKY_MANUAL_LOGIN_RESPONSE_TIMEOUT_MS
  170. );
  171. return {
  172. timeoutMs: extendedTimeoutMs,
  173. responseTimeoutMs: extendedTimeoutMs,
  174. };
  175. }
  176. return {
  177. timeoutMs: timedPoll.timeoutMs,
  178. responseTimeoutMs: timedPoll.responseTimeoutMs,
  179. };
  180. }
  181. async function requestVerificationCodeResend(step, options = {}) {
  182. throwIfStopped();
  183. const signupTabId = await getTabId('signup-page');
  184. if (!signupTabId) {
  185. throw new Error('认证页面标签页已关闭,无法重新请求验证码。');
  186. }
  187. throwIfStopped();
  188. await chrome.tabs.update(signupTabId, { active: true });
  189. throwIfStopped();
  190. const result = await sendToContentScript('signup-page', {
  191. type: 'RESEND_VERIFICATION_CODE',
  192. step,
  193. source: 'background',
  194. payload: {},
  195. }, {
  196. responseTimeoutMs: await getResponseTimeoutMsForStep(
  197. step,
  198. options,
  199. 30000,
  200. `重新发送${getVerificationCodeLabel(step)}验证码`
  201. ),
  202. });
  203. if (result && result.error) {
  204. throw new Error(result.error);
  205. }
  206. await addLog(`步骤 ${step}:已请求新的${getVerificationCodeLabel(step)}验证码。`, 'warn');
  207. const requestedAt = Date.now();
  208. if (step === 4) {
  209. await setState({ signupVerificationRequestedAt: requestedAt });
  210. }
  211. if (step === 8) {
  212. await setState({ loginVerificationRequestedAt: requestedAt });
  213. }
  214. const currentState = await getState();
  215. if (currentState.mailProvider === '2925') {
  216. const mailTabId = await getTabId('mail-2925');
  217. if (mailTabId) {
  218. await chrome.tabs.update(mailTabId, { active: true });
  219. await addLog(`步骤 ${step}:已切换到 2925 邮箱标签页等待新邮件。`, 'info');
  220. }
  221. }
  222. return requestedAt;
  223. }
  224. async function pollFreshVerificationCodeWithResendInterval(step, state, mail, pollOverrides = {}) {
  225. const stateKey = getVerificationCodeStateKey(step);
  226. const rejectedCodes = new Set();
  227. if (state[stateKey]) {
  228. rejectedCodes.add(state[stateKey]);
  229. }
  230. for (const code of (pollOverrides.excludeCodes || [])) {
  231. if (code) rejectedCodes.add(code);
  232. }
  233. const {
  234. maxRounds: _ignoredMaxRounds,
  235. maxResendRequests: _ignoredMaxResendRequests,
  236. resendIntervalMs: _ignoredResendIntervalMs,
  237. lastResendAt: _ignoredLastResendAt,
  238. onResendRequestedAt: _ignoredOnResendRequestedAt,
  239. ...payloadOverrides
  240. } = pollOverrides;
  241. const onResendRequestedAt = typeof pollOverrides.onResendRequestedAt === 'function'
  242. ? pollOverrides.onResendRequestedAt
  243. : null;
  244. let lastError = null;
  245. let filterAfterTimestamp = payloadOverrides.filterAfterTimestamp ?? getVerificationPollPayload(step, state).filterAfterTimestamp;
  246. const maxResendRequests = resolveMaxResendRequests(pollOverrides);
  247. const totalRounds = maxResendRequests + 1;
  248. const maxRounds = totalRounds;
  249. const resendIntervalMs = Math.max(0, Number(pollOverrides.resendIntervalMs) || 0);
  250. let lastResendAt = Number(pollOverrides.lastResendAt) || 0;
  251. let usedResendRequests = 0;
  252. for (let round = 1; round <= totalRounds; round++) {
  253. throwIfStopped();
  254. if (round > 1) {
  255. lastResendAt = await requestVerificationCodeResend(step, pollOverrides);
  256. usedResendRequests += 1;
  257. if (onResendRequestedAt) {
  258. const nextFilterAfterTimestamp = await onResendRequestedAt(lastResendAt);
  259. if (nextFilterAfterTimestamp !== undefined) {
  260. filterAfterTimestamp = nextFilterAfterTimestamp;
  261. }
  262. }
  263. }
  264. while (true) {
  265. throwIfStopped();
  266. const payload = getVerificationPollPayload(step, state, {
  267. ...payloadOverrides,
  268. filterAfterTimestamp,
  269. excludeCodes: [...rejectedCodes],
  270. });
  271. if (lastResendAt > 0) {
  272. const remainingBeforeResendMs = Math.max(0, resendIntervalMs - (Date.now() - lastResendAt));
  273. const baseMaxAttempts = Math.max(1, Number(payload.maxAttempts) || 5);
  274. const intervalMs = Math.max(1, Number(payload.intervalMs) || 3000);
  275. payload.maxAttempts = Math.max(1, Math.min(baseMaxAttempts, Math.floor(remainingBeforeResendMs / intervalMs) + 1));
  276. }
  277. try {
  278. const timedPoll = await applyMailPollingTimeBudget(
  279. step,
  280. payload,
  281. pollOverrides,
  282. `轮询${getVerificationCodeLabel(step)}验证码邮箱`
  283. );
  284. const result = await sendToMailContentScriptResilient(
  285. mail,
  286. {
  287. type: 'POLL_EMAIL',
  288. step,
  289. source: 'background',
  290. payload: timedPoll.payload,
  291. },
  292. {
  293. ...getMailContentScriptTimeoutOptions(mail, timedPoll),
  294. maxRecoveryAttempts: 2,
  295. }
  296. );
  297. if (result && result.error) {
  298. throw new Error(result.error);
  299. }
  300. if (!result || !result.code) {
  301. throw new Error(`步骤 ${step}:邮箱轮询结束,但未获取到验证码。`);
  302. }
  303. if (rejectedCodes.has(result.code)) {
  304. throw new Error(`步骤 ${step}:再次收到了相同的${getVerificationCodeLabel(step)}验证码:${result.code}`);
  305. }
  306. return {
  307. ...result,
  308. lastResendAt,
  309. remainingResendRequests: Math.max(0, maxResendRequests - usedResendRequests),
  310. };
  311. } catch (err) {
  312. if (isStopError(err)) {
  313. throw err;
  314. }
  315. lastError = err;
  316. await addLog(`步骤 ${step}:${err.message}`, 'warn');
  317. }
  318. const remainingBeforeResendMs = lastResendAt > 0
  319. ? Math.max(0, resendIntervalMs - (Date.now() - lastResendAt))
  320. : 0;
  321. if (remainingBeforeResendMs > 0) {
  322. await addLog(
  323. `步骤 ${step}:距离下次重新发送验证码还差 ${Math.ceil(remainingBeforeResendMs / 1000)} 秒,继续刷新邮箱(第 ${round}/${maxRounds} 轮)...`,
  324. 'info'
  325. );
  326. continue;
  327. }
  328. if (round < maxRounds) {
  329. await addLog(`步骤 ${step}:已到 25 秒重发间隔,准备重新发送验证码(第 ${round + 1}/${maxRounds} 轮)...`, 'warn');
  330. }
  331. break;
  332. }
  333. }
  334. throw lastError || new Error(`步骤 ${step}:无法获取新的${getVerificationCodeLabel(step)}验证码。`);
  335. }
  336. async function pollFreshVerificationCode(step, state, mail, pollOverrides = {}) {
  337. const {
  338. onResendRequestedAt,
  339. maxRounds: _ignoredMaxRounds,
  340. maxResendRequests: _ignoredMaxResendRequests,
  341. ...cleanPollOverrides
  342. } = pollOverrides;
  343. if (mail.provider === A4SKY_PROVIDER) {
  344. const timedPoll = await applyMailPollingTimeBudget(step, {
  345. ...getVerificationPollPayload(step, state),
  346. ...cleanPollOverrides,
  347. }, cleanPollOverrides, `轮询${getVerificationCodeLabel(step)}验证码邮箱`);
  348. return pollA4skyImapVerificationCode(step, state, timedPoll.payload);
  349. }
  350. if (mail.provider === HOTMAIL_PROVIDER) {
  351. const hotmailPollConfig = getHotmailVerificationPollConfig(step);
  352. const timedPoll = await applyMailPollingTimeBudget(step, {
  353. ...getVerificationPollPayload(step, state),
  354. ...hotmailPollConfig,
  355. ...cleanPollOverrides,
  356. }, cleanPollOverrides, `轮询${getVerificationCodeLabel(step)}验证码邮箱`);
  357. return pollHotmailVerificationCode(step, state, timedPoll.payload);
  358. }
  359. if (mail.provider === LUCKMAIL_PROVIDER) {
  360. const timedPoll = await applyMailPollingTimeBudget(step, {
  361. ...getVerificationPollPayload(step, state),
  362. ...cleanPollOverrides,
  363. }, cleanPollOverrides, `轮询${getVerificationCodeLabel(step)}验证码邮箱`);
  364. return pollLuckmailVerificationCode(step, state, timedPoll.payload);
  365. }
  366. if (mail.provider === CLOUDFLARE_TEMP_EMAIL_PROVIDER) {
  367. const timedPoll = await applyMailPollingTimeBudget(step, {
  368. ...getVerificationPollPayload(step, state),
  369. ...cleanPollOverrides,
  370. }, cleanPollOverrides, `轮询${getVerificationCodeLabel(step)}验证码邮箱`);
  371. return pollCloudflareTempEmailVerificationCode(step, state, timedPoll.payload);
  372. }
  373. if (Number(pollOverrides.resendIntervalMs) > 0) {
  374. return pollFreshVerificationCodeWithResendInterval(step, state, mail, pollOverrides);
  375. }
  376. const stateKey = getVerificationCodeStateKey(step);
  377. const rejectedCodes = new Set();
  378. if (state[stateKey]) {
  379. rejectedCodes.add(state[stateKey]);
  380. }
  381. for (const code of (pollOverrides.excludeCodes || [])) {
  382. if (code) rejectedCodes.add(code);
  383. }
  384. let lastError = null;
  385. let filterAfterTimestamp = cleanPollOverrides.filterAfterTimestamp ?? getVerificationPollPayload(step, state).filterAfterTimestamp;
  386. const maxResendRequests = resolveMaxResendRequests(pollOverrides);
  387. const maxRounds = maxResendRequests + 1;
  388. let usedResendRequests = 0;
  389. for (let round = 1; round <= maxRounds; round++) {
  390. throwIfStopped();
  391. if (round > 1) {
  392. const requestedAt = await requestVerificationCodeResend(step, pollOverrides);
  393. usedResendRequests += 1;
  394. if (typeof onResendRequestedAt === 'function') {
  395. const nextFilterAfterTimestamp = await onResendRequestedAt(requestedAt);
  396. if (nextFilterAfterTimestamp !== undefined) {
  397. filterAfterTimestamp = nextFilterAfterTimestamp;
  398. }
  399. }
  400. }
  401. const payload = getVerificationPollPayload(step, state, {
  402. ...cleanPollOverrides,
  403. filterAfterTimestamp,
  404. excludeCodes: [...rejectedCodes],
  405. });
  406. try {
  407. const timedPoll = await applyMailPollingTimeBudget(
  408. step,
  409. payload,
  410. pollOverrides,
  411. `轮询${getVerificationCodeLabel(step)}验证码邮箱`
  412. );
  413. const result = await sendToMailContentScriptResilient(
  414. mail,
  415. {
  416. type: 'POLL_EMAIL',
  417. step,
  418. source: 'background',
  419. payload: timedPoll.payload,
  420. },
  421. {
  422. ...getMailContentScriptTimeoutOptions(mail, timedPoll),
  423. maxRecoveryAttempts: 2,
  424. }
  425. );
  426. if (result && result.error) {
  427. throw new Error(result.error);
  428. }
  429. if (!result || !result.code) {
  430. throw new Error(`步骤 ${step}:邮箱轮询结束,但未获取到验证码。`);
  431. }
  432. if (rejectedCodes.has(result.code)) {
  433. throw new Error(`步骤 ${step}:再次收到了相同的${getVerificationCodeLabel(step)}验证码:${result.code}`);
  434. }
  435. return {
  436. ...result,
  437. remainingResendRequests: Math.max(0, maxResendRequests - usedResendRequests),
  438. };
  439. } catch (err) {
  440. if (isStopError(err)) {
  441. throw err;
  442. }
  443. lastError = err;
  444. await addLog(`步骤 ${step}:${err.message}`, 'warn');
  445. if (round < maxRounds) {
  446. await addLog(`步骤 ${step}:将重新发送验证码后重试(${round + 1}/${maxRounds})...`, 'warn');
  447. }
  448. }
  449. }
  450. throw lastError || new Error(`步骤 ${step}:无法获取新的${getVerificationCodeLabel(step)}验证码。`);
  451. }
  452. async function submitVerificationCode(step, code, options = {}) {
  453. const signupTabId = await getTabId('signup-page');
  454. if (!signupTabId) {
  455. throw new Error('认证页面标签页已关闭,无法填写验证码。');
  456. }
  457. await chrome.tabs.update(signupTabId, { active: true });
  458. const result = await sendToContentScript('signup-page', {
  459. type: 'FILL_CODE',
  460. step,
  461. source: 'background',
  462. payload: { code },
  463. }, {
  464. responseTimeoutMs: await getResponseTimeoutMsForStep(
  465. step,
  466. options,
  467. step === 7 ? 45000 : 30000,
  468. `填写${getVerificationCodeLabel(step)}验证码`
  469. ),
  470. });
  471. if (result && result.error) {
  472. throw new Error(result.error);
  473. }
  474. return result || {};
  475. }
  476. async function resolveVerificationStep(step, state, mail, options = {}) {
  477. const stateKey = getVerificationCodeStateKey(step);
  478. const rejectedCodes = new Set();
  479. const hotmailPollConfig = mail.provider === HOTMAIL_PROVIDER
  480. ? getHotmailVerificationPollConfig(step)
  481. : null;
  482. const beforeSubmit = typeof options.beforeSubmit === 'function'
  483. ? options.beforeSubmit
  484. : null;
  485. const ignorePersistedLastCode = Boolean(hotmailPollConfig?.ignorePersistedLastCode);
  486. if (state[stateKey] && !ignorePersistedLastCode) {
  487. rejectedCodes.add(state[stateKey]);
  488. }
  489. let nextFilterAfterTimestamp = options.filterAfterTimestamp ?? null;
  490. const requestFreshCodeFirst = options.requestFreshCodeFirst !== undefined
  491. ? Boolean(options.requestFreshCodeFirst)
  492. : (hotmailPollConfig?.requestFreshCodeFirst ?? false);
  493. let remainingAutomaticResendCount = options.maxResendRequests !== undefined
  494. ? normalizeVerificationResendCount(
  495. options.maxResendRequests,
  496. getLegacyVerificationResendCountDefault(step, { requestFreshCodeFirst })
  497. )
  498. : getConfiguredVerificationResendCount(step, state, { requestFreshCodeFirst });
  499. const maxSubmitAttempts = 3;
  500. const resendIntervalMs = Math.max(0, Number(options.resendIntervalMs) || 0);
  501. let lastResendAt = Number(options.lastResendAt) || 0;
  502. const updateFilterAfterTimestampForVerificationStep = async (_requestedAt) => {
  503. return nextFilterAfterTimestamp;
  504. };
  505. if (requestFreshCodeFirst) {
  506. if (remainingAutomaticResendCount <= 0) {
  507. await addLog(`步骤 ${step}:当前自动重新发送验证码次数为 0,将直接使用当前时间窗口轮询邮箱。`, 'info');
  508. } else {
  509. try {
  510. lastResendAt = await requestVerificationCodeResend(step, options);
  511. remainingAutomaticResendCount -= 1;
  512. await updateFilterAfterTimestampForVerificationStep(lastResendAt);
  513. await addLog(`步骤 ${step}:已先请求一封新的${getVerificationCodeLabel(step)}验证码,再开始轮询邮箱。`, 'warn');
  514. } catch (err) {
  515. if (isStopError(err)) {
  516. throw err;
  517. }
  518. await addLog(`步骤 ${step}:首次重新获取验证码失败:${err.message},将继续使用当前时间窗口轮询。`, 'warn');
  519. }
  520. }
  521. }
  522. if (mail.provider === HOTMAIL_PROVIDER) {
  523. const initialDelayMs = Number(options.initialDelayMs ?? hotmailPollConfig.initialDelayMs) || 0;
  524. if (initialDelayMs > 0) {
  525. const remainingMs = await getRemainingTimeBudgetMs(
  526. step,
  527. options,
  528. `等待${getVerificationCodeLabel(step)}验证码邮件到达`
  529. );
  530. const delayMs = remainingMs === null
  531. ? initialDelayMs
  532. : Math.min(initialDelayMs, Math.max(0, remainingMs));
  533. await addLog(`步骤 ${step}:等待 ${Math.round(initialDelayMs / 1000)} 秒,让 Hotmail 验证码邮件先到达...`, 'info');
  534. await sleepWithStop(delayMs);
  535. }
  536. }
  537. for (let attempt = 1; attempt <= maxSubmitAttempts; attempt++) {
  538. const pollOptions = {
  539. excludeCodes: [...rejectedCodes],
  540. getRemainingTimeMs: options.getRemainingTimeMs,
  541. maxResendRequests: remainingAutomaticResendCount,
  542. resendIntervalMs,
  543. lastResendAt,
  544. onResendRequestedAt: updateFilterAfterTimestampForVerificationStep,
  545. };
  546. if (nextFilterAfterTimestamp !== null && nextFilterAfterTimestamp !== undefined) {
  547. pollOptions.filterAfterTimestamp = nextFilterAfterTimestamp;
  548. }
  549. const result = await pollFreshVerificationCode(step, state, mail, pollOptions);
  550. lastResendAt = Number(result?.lastResendAt) || lastResendAt;
  551. remainingAutomaticResendCount = normalizeVerificationResendCount(
  552. result?.remainingResendRequests,
  553. remainingAutomaticResendCount
  554. );
  555. throwIfStopped();
  556. await addLog(`步骤 ${step}:已获取${getVerificationCodeLabel(step)}验证码:${result.code}`);
  557. if (beforeSubmit) {
  558. await beforeSubmit(result, {
  559. attempt,
  560. rejectedCodes: new Set(rejectedCodes),
  561. filterAfterTimestamp: nextFilterAfterTimestamp ?? undefined,
  562. lastResendAt,
  563. });
  564. }
  565. throwIfStopped();
  566. await setState({
  567. [stateKey]: result.code,
  568. });
  569. const submitResult = await submitVerificationCode(step, result.code, options);
  570. if (submitResult.invalidCode) {
  571. rejectedCodes.add(result.code);
  572. await addLog(`步骤 ${step}:验证码被页面拒绝:${submitResult.errorText || result.code}`, 'warn');
  573. if (attempt >= maxSubmitAttempts) {
  574. throw new Error(`步骤 ${step}:验证码连续失败,已达到 ${maxSubmitAttempts} 次重试上限。`);
  575. }
  576. const remainingBeforeResendMs = resendIntervalMs > 0 && lastResendAt > 0
  577. ? Math.max(0, resendIntervalMs - (Date.now() - lastResendAt))
  578. : 0;
  579. if (remainingBeforeResendMs > 0) {
  580. await addLog(
  581. `步骤 ${step}:提交失败后距离下次重新发送验证码还差 ${Math.ceil(remainingBeforeResendMs / 1000)} 秒,先继续刷新邮箱(${attempt + 1}/${maxSubmitAttempts})...`,
  582. 'warn'
  583. );
  584. continue;
  585. }
  586. if (remainingAutomaticResendCount <= 0) {
  587. await addLog(`步骤 ${step}:已达到自动重新发送验证码次数上限,将直接使用当前时间窗口继续重试。`, 'warn');
  588. continue;
  589. }
  590. lastResendAt = await requestVerificationCodeResend(step, options);
  591. remainingAutomaticResendCount -= 1;
  592. await updateFilterAfterTimestampForVerificationStep(lastResendAt);
  593. await addLog(`步骤 ${step}:提交失败后已请求新验证码(${attempt + 1}/${maxSubmitAttempts})...`, 'warn');
  594. continue;
  595. }
  596. if (submitResult.addPhonePage) {
  597. const urlPart = submitResult.url ? ` URL: ${submitResult.url}` : '';
  598. throw new Error(`步骤 ${step}:验证码提交后页面进入手机号页面,当前流程无法继续自动授权。${urlPart}`.trim());
  599. }
  600. await setState({
  601. lastEmailTimestamp: result.emailTimestamp,
  602. [stateKey]: result.code,
  603. });
  604. await completeStepFromBackground(step, {
  605. emailTimestamp: result.emailTimestamp,
  606. code: result.code,
  607. });
  608. return;
  609. }
  610. }
  611. return {
  612. confirmCustomVerificationStepBypass,
  613. getVerificationCodeLabel,
  614. getVerificationCodeStateKey,
  615. getVerificationPollPayload,
  616. pollFreshVerificationCode,
  617. pollFreshVerificationCodeWithResendInterval,
  618. requestVerificationCodeResend,
  619. resolveVerificationStep,
  620. submitVerificationCode,
  621. };
  622. }
  623. return {
  624. createVerificationFlowHelpers,
  625. };
  626. });