verification-flow.js 26 KB

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