verification-flow.js 26 KB

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