verification-flow.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513
  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. async function confirmCustomVerificationStepBypass(step) {
  37. const verificationLabel = getVerificationCodeLabel(step);
  38. await addLog(`步骤 ${step}:当前为自定义邮箱模式,请手动在页面中输入${verificationLabel}验证码并进入下一页面。`, 'warn');
  39. let response = null;
  40. try {
  41. response = await confirmCustomVerificationStepBypassRequest(step);
  42. } catch {
  43. throw new Error(`步骤 ${step}:无法打开确认弹窗,请先保持侧边栏打开后重试。`);
  44. }
  45. if (response?.error) {
  46. throw new Error(response.error);
  47. }
  48. if (!response?.confirmed) {
  49. throw new Error(`步骤 ${step}:已取消手动${verificationLabel}验证码确认。`);
  50. }
  51. await setState({
  52. lastEmailTimestamp: null,
  53. signupVerificationRequestedAt: null,
  54. loginVerificationRequestedAt: null,
  55. });
  56. await deps.setStepStatus(step, 'skipped');
  57. await addLog(`步骤 ${step}:已确认手动完成${verificationLabel}验证码输入,当前步骤已跳过。`, 'warn');
  58. }
  59. function getVerificationPollPayload(step, state, overrides = {}) {
  60. const is2925Provider = state?.mailProvider === '2925';
  61. if (step === 4) {
  62. return {
  63. filterAfterTimestamp: getHotmailVerificationRequestTimestamp(4, state),
  64. senderFilters: ['openai', 'noreply', 'verify', 'auth', 'duckduckgo', 'forward'],
  65. subjectFilters: ['verify', 'verification', 'code', '验证码', 'confirm'],
  66. targetEmail: state.email,
  67. maxAttempts: is2925Provider ? MAIL_2925_VERIFICATION_MAX_ATTEMPTS : 5,
  68. intervalMs: is2925Provider ? MAIL_2925_VERIFICATION_INTERVAL_MS : 3000,
  69. ...overrides,
  70. };
  71. }
  72. return {
  73. filterAfterTimestamp: getHotmailVerificationRequestTimestamp(7, state),
  74. senderFilters: ['openai', 'noreply', 'verify', 'auth', 'chatgpt', 'duckduckgo', 'forward'],
  75. subjectFilters: ['verify', 'verification', 'code', '验证码', 'confirm', 'login'],
  76. targetEmail: state.email,
  77. maxAttempts: is2925Provider ? MAIL_2925_VERIFICATION_MAX_ATTEMPTS : 5,
  78. intervalMs: is2925Provider ? MAIL_2925_VERIFICATION_INTERVAL_MS : 3000,
  79. ...overrides,
  80. };
  81. }
  82. async function requestVerificationCodeResend(step) {
  83. throwIfStopped();
  84. const signupTabId = await getTabId('signup-page');
  85. if (!signupTabId) {
  86. throw new Error('认证页面标签页已关闭,无法重新请求验证码。');
  87. }
  88. throwIfStopped();
  89. await chrome.tabs.update(signupTabId, { active: true });
  90. throwIfStopped();
  91. const result = await sendToContentScript('signup-page', {
  92. type: 'RESEND_VERIFICATION_CODE',
  93. step,
  94. source: 'background',
  95. payload: {},
  96. });
  97. if (result && result.error) {
  98. throw new Error(result.error);
  99. }
  100. await addLog(`步骤 ${step}:已请求新的${getVerificationCodeLabel(step)}验证码。`, 'warn');
  101. const requestedAt = Date.now();
  102. if (step === 4) {
  103. await setState({ signupVerificationRequestedAt: requestedAt });
  104. }
  105. if (step === 7) {
  106. await setState({ loginVerificationRequestedAt: requestedAt });
  107. }
  108. const currentState = await getState();
  109. if (currentState.mailProvider === '2925') {
  110. const mailTabId = await getTabId('mail-2925');
  111. if (mailTabId) {
  112. await chrome.tabs.update(mailTabId, { active: true });
  113. await addLog(`步骤 ${step}:已切换到 2925 邮箱标签页等待新邮件。`, 'info');
  114. }
  115. }
  116. return requestedAt;
  117. }
  118. async function pollFreshVerificationCodeWithResendInterval(step, state, mail, pollOverrides = {}) {
  119. const stateKey = getVerificationCodeStateKey(step);
  120. const rejectedCodes = new Set();
  121. if (state[stateKey]) {
  122. rejectedCodes.add(state[stateKey]);
  123. }
  124. for (const code of (pollOverrides.excludeCodes || [])) {
  125. if (code) rejectedCodes.add(code);
  126. }
  127. const {
  128. maxRounds: _ignoredMaxRounds,
  129. resendIntervalMs: _ignoredResendIntervalMs,
  130. lastResendAt: _ignoredLastResendAt,
  131. onResendRequestedAt: _ignoredOnResendRequestedAt,
  132. ...payloadOverrides
  133. } = pollOverrides;
  134. const onResendRequestedAt = typeof pollOverrides.onResendRequestedAt === 'function'
  135. ? pollOverrides.onResendRequestedAt
  136. : null;
  137. let lastError = null;
  138. let filterAfterTimestamp = payloadOverrides.filterAfterTimestamp ?? getVerificationPollPayload(step, state).filterAfterTimestamp;
  139. const maxRounds = pollOverrides.maxRounds || VERIFICATION_POLL_MAX_ROUNDS;
  140. const resendIntervalMs = Math.max(0, Number(pollOverrides.resendIntervalMs) || 0);
  141. let lastResendAt = Number(pollOverrides.lastResendAt) || 0;
  142. for (let round = 1; round <= maxRounds; round++) {
  143. throwIfStopped();
  144. if (round > 1) {
  145. lastResendAt = await requestVerificationCodeResend(step);
  146. if (onResendRequestedAt) {
  147. const nextFilterAfterTimestamp = await onResendRequestedAt(lastResendAt);
  148. if (nextFilterAfterTimestamp !== undefined) {
  149. filterAfterTimestamp = nextFilterAfterTimestamp;
  150. }
  151. }
  152. }
  153. while (true) {
  154. throwIfStopped();
  155. const payload = getVerificationPollPayload(step, state, {
  156. ...payloadOverrides,
  157. filterAfterTimestamp,
  158. excludeCodes: [...rejectedCodes],
  159. });
  160. if (lastResendAt > 0) {
  161. const remainingBeforeResendMs = Math.max(0, resendIntervalMs - (Date.now() - lastResendAt));
  162. const baseMaxAttempts = Math.max(1, Number(payload.maxAttempts) || 5);
  163. const intervalMs = Math.max(1, Number(payload.intervalMs) || 3000);
  164. payload.maxAttempts = Math.max(1, Math.min(baseMaxAttempts, Math.floor(remainingBeforeResendMs / intervalMs) + 1));
  165. }
  166. try {
  167. const result = await sendToMailContentScriptResilient(
  168. mail,
  169. {
  170. type: 'POLL_EMAIL',
  171. step,
  172. source: 'background',
  173. payload,
  174. },
  175. {
  176. timeoutMs: 45000,
  177. maxRecoveryAttempts: 2,
  178. }
  179. );
  180. if (result && result.error) {
  181. throw new Error(result.error);
  182. }
  183. if (!result || !result.code) {
  184. throw new Error(`步骤 ${step}:邮箱轮询结束,但未获取到验证码。`);
  185. }
  186. if (rejectedCodes.has(result.code)) {
  187. throw new Error(`步骤 ${step}:再次收到了相同的${getVerificationCodeLabel(step)}验证码:${result.code}`);
  188. }
  189. return {
  190. ...result,
  191. lastResendAt,
  192. };
  193. } catch (err) {
  194. if (isStopError(err)) {
  195. throw err;
  196. }
  197. lastError = err;
  198. await addLog(`步骤 ${step}:${err.message}`, 'warn');
  199. }
  200. const remainingBeforeResendMs = lastResendAt > 0
  201. ? Math.max(0, resendIntervalMs - (Date.now() - lastResendAt))
  202. : 0;
  203. if (remainingBeforeResendMs > 0) {
  204. await addLog(
  205. `步骤 ${step}:距离下次重新发送验证码还差 ${Math.ceil(remainingBeforeResendMs / 1000)} 秒,继续刷新邮箱(第 ${round}/${maxRounds} 轮)...`,
  206. 'info'
  207. );
  208. continue;
  209. }
  210. if (round < maxRounds) {
  211. await addLog(`步骤 ${step}:已到 25 秒重发间隔,准备重新发送验证码(第 ${round + 1}/${maxRounds} 轮)...`, 'warn');
  212. }
  213. break;
  214. }
  215. }
  216. throw lastError || new Error(`步骤 ${step}:无法获取新的${getVerificationCodeLabel(step)}验证码。`);
  217. }
  218. async function pollFreshVerificationCode(step, state, mail, pollOverrides = {}) {
  219. const { onResendRequestedAt, ...cleanPollOverrides } = pollOverrides;
  220. if (mail.provider === HOTMAIL_PROVIDER) {
  221. const hotmailPollConfig = getHotmailVerificationPollConfig(step);
  222. return pollHotmailVerificationCode(step, state, {
  223. ...getVerificationPollPayload(step, state),
  224. ...hotmailPollConfig,
  225. ...cleanPollOverrides,
  226. });
  227. }
  228. if (mail.provider === LUCKMAIL_PROVIDER) {
  229. return pollLuckmailVerificationCode(step, state, {
  230. ...getVerificationPollPayload(step, state),
  231. ...pollOverrides,
  232. });
  233. }
  234. if (mail.provider === CLOUDFLARE_TEMP_EMAIL_PROVIDER) {
  235. return pollCloudflareTempEmailVerificationCode(step, state, {
  236. ...getVerificationPollPayload(step, state),
  237. ...pollOverrides,
  238. });
  239. }
  240. if (Number(pollOverrides.resendIntervalMs) > 0) {
  241. return pollFreshVerificationCodeWithResendInterval(step, state, mail, pollOverrides);
  242. }
  243. const stateKey = getVerificationCodeStateKey(step);
  244. const rejectedCodes = new Set();
  245. if (state[stateKey]) {
  246. rejectedCodes.add(state[stateKey]);
  247. }
  248. for (const code of (pollOverrides.excludeCodes || [])) {
  249. if (code) rejectedCodes.add(code);
  250. }
  251. let lastError = null;
  252. let filterAfterTimestamp = cleanPollOverrides.filterAfterTimestamp ?? getVerificationPollPayload(step, state).filterAfterTimestamp;
  253. const maxRounds = pollOverrides.maxRounds || VERIFICATION_POLL_MAX_ROUNDS;
  254. for (let round = 1; round <= maxRounds; round++) {
  255. throwIfStopped();
  256. if (round > 1) {
  257. const requestedAt = await requestVerificationCodeResend(step);
  258. if (typeof onResendRequestedAt === 'function') {
  259. const nextFilterAfterTimestamp = await onResendRequestedAt(requestedAt);
  260. if (nextFilterAfterTimestamp !== undefined) {
  261. filterAfterTimestamp = nextFilterAfterTimestamp;
  262. }
  263. }
  264. }
  265. const payload = getVerificationPollPayload(step, state, {
  266. ...cleanPollOverrides,
  267. filterAfterTimestamp,
  268. excludeCodes: [...rejectedCodes],
  269. });
  270. try {
  271. const result = await sendToMailContentScriptResilient(
  272. mail,
  273. {
  274. type: 'POLL_EMAIL',
  275. step,
  276. source: 'background',
  277. payload,
  278. },
  279. {
  280. timeoutMs: 45000,
  281. maxRecoveryAttempts: 2,
  282. }
  283. );
  284. if (result && result.error) {
  285. throw new Error(result.error);
  286. }
  287. if (!result || !result.code) {
  288. throw new Error(`步骤 ${step}:邮箱轮询结束,但未获取到验证码。`);
  289. }
  290. if (rejectedCodes.has(result.code)) {
  291. throw new Error(`步骤 ${step}:再次收到了相同的${getVerificationCodeLabel(step)}验证码:${result.code}`);
  292. }
  293. return result;
  294. } catch (err) {
  295. if (isStopError(err)) {
  296. throw err;
  297. }
  298. lastError = err;
  299. await addLog(`步骤 ${step}:${err.message}`, 'warn');
  300. if (round < maxRounds) {
  301. await addLog(`步骤 ${step}:将重新发送验证码后重试(${round + 1}/${maxRounds})...`, 'warn');
  302. }
  303. }
  304. }
  305. throw lastError || new Error(`步骤 ${step}:无法获取新的${getVerificationCodeLabel(step)}验证码。`);
  306. }
  307. async function submitVerificationCode(step, code) {
  308. const signupTabId = await getTabId('signup-page');
  309. if (!signupTabId) {
  310. throw new Error('认证页面标签页已关闭,无法填写验证码。');
  311. }
  312. await chrome.tabs.update(signupTabId, { active: true });
  313. const result = await sendToContentScript('signup-page', {
  314. type: 'FILL_CODE',
  315. step,
  316. source: 'background',
  317. payload: { code },
  318. });
  319. if (result && result.error) {
  320. throw new Error(result.error);
  321. }
  322. return result || {};
  323. }
  324. async function resolveVerificationStep(step, state, mail, options = {}) {
  325. const stateKey = getVerificationCodeStateKey(step);
  326. const rejectedCodes = new Set();
  327. const hotmailPollConfig = mail.provider === HOTMAIL_PROVIDER
  328. ? getHotmailVerificationPollConfig(step)
  329. : null;
  330. const beforeSubmit = typeof options.beforeSubmit === 'function'
  331. ? options.beforeSubmit
  332. : null;
  333. const ignorePersistedLastCode = Boolean(hotmailPollConfig?.ignorePersistedLastCode);
  334. if (state[stateKey] && !ignorePersistedLastCode) {
  335. rejectedCodes.add(state[stateKey]);
  336. }
  337. let nextFilterAfterTimestamp = options.filterAfterTimestamp ?? null;
  338. const requestFreshCodeFirst = options.requestFreshCodeFirst !== undefined
  339. ? Boolean(options.requestFreshCodeFirst)
  340. : (hotmailPollConfig?.requestFreshCodeFirst ?? false);
  341. const maxSubmitAttempts = 3;
  342. const resendIntervalMs = Math.max(0, Number(options.resendIntervalMs) || 0);
  343. let lastResendAt = Number(options.lastResendAt) || 0;
  344. const updateFilterAfterTimestampForVerificationStep = async (requestedAt) => {
  345. if ((step !== 4 && step !== 7) || !requestedAt) {
  346. return nextFilterAfterTimestamp;
  347. }
  348. if (mail.provider === HOTMAIL_PROVIDER) {
  349. nextFilterAfterTimestamp = getHotmailVerificationRequestTimestamp(step, {
  350. ...state,
  351. ...(step === 4
  352. ? { signupVerificationRequestedAt: requestedAt }
  353. : { loginVerificationRequestedAt: requestedAt }),
  354. });
  355. } else {
  356. nextFilterAfterTimestamp = Math.max(0, Number(requestedAt) - 60000);
  357. }
  358. return nextFilterAfterTimestamp;
  359. };
  360. if (requestFreshCodeFirst) {
  361. try {
  362. lastResendAt = await requestVerificationCodeResend(step);
  363. await updateFilterAfterTimestampForVerificationStep(lastResendAt);
  364. await addLog(`步骤 ${step}:已先请求一封新的${getVerificationCodeLabel(step)}验证码,再开始轮询邮箱。`, 'warn');
  365. } catch (err) {
  366. if (isStopError(err)) {
  367. throw err;
  368. }
  369. await addLog(`步骤 ${step}:首次重新获取验证码失败:${err.message},将继续使用当前时间窗口轮询。`, 'warn');
  370. }
  371. }
  372. if (mail.provider === HOTMAIL_PROVIDER) {
  373. const initialDelayMs = Number(options.initialDelayMs ?? hotmailPollConfig.initialDelayMs) || 0;
  374. if (initialDelayMs > 0) {
  375. await addLog(`步骤 ${step}:等待 ${Math.round(initialDelayMs / 1000)} 秒,让 Hotmail 验证码邮件先到达...`, 'info');
  376. await sleepWithStop(initialDelayMs);
  377. }
  378. }
  379. for (let attempt = 1; attempt <= maxSubmitAttempts; attempt++) {
  380. const pollOptions = {
  381. excludeCodes: [...rejectedCodes],
  382. resendIntervalMs,
  383. lastResendAt,
  384. onResendRequestedAt: updateFilterAfterTimestampForVerificationStep,
  385. };
  386. if (nextFilterAfterTimestamp !== null && nextFilterAfterTimestamp !== undefined) {
  387. pollOptions.filterAfterTimestamp = nextFilterAfterTimestamp;
  388. }
  389. const result = await pollFreshVerificationCode(step, state, mail, pollOptions);
  390. lastResendAt = Number(result?.lastResendAt) || lastResendAt;
  391. throwIfStopped();
  392. await addLog(`步骤 ${step}:已获取${getVerificationCodeLabel(step)}验证码:${result.code}`);
  393. if (beforeSubmit) {
  394. await beforeSubmit(result, {
  395. attempt,
  396. rejectedCodes: new Set(rejectedCodes),
  397. filterAfterTimestamp: nextFilterAfterTimestamp ?? undefined,
  398. lastResendAt,
  399. });
  400. }
  401. throwIfStopped();
  402. const submitResult = await submitVerificationCode(step, result.code);
  403. if (submitResult.invalidCode) {
  404. rejectedCodes.add(result.code);
  405. await addLog(`步骤 ${step}:验证码被页面拒绝:${submitResult.errorText || result.code}`, 'warn');
  406. if (attempt >= maxSubmitAttempts) {
  407. throw new Error(`步骤 ${step}:验证码连续失败,已达到 ${maxSubmitAttempts} 次重试上限。`);
  408. }
  409. const remainingBeforeResendMs = resendIntervalMs > 0 && lastResendAt > 0
  410. ? Math.max(0, resendIntervalMs - (Date.now() - lastResendAt))
  411. : 0;
  412. if (remainingBeforeResendMs > 0) {
  413. await addLog(
  414. `步骤 ${step}:提交失败后距离下次重新发送验证码还差 ${Math.ceil(remainingBeforeResendMs / 1000)} 秒,先继续刷新邮箱(${attempt + 1}/${maxSubmitAttempts})...`,
  415. 'warn'
  416. );
  417. continue;
  418. }
  419. lastResendAt = await requestVerificationCodeResend(step);
  420. await updateFilterAfterTimestampForVerificationStep(lastResendAt);
  421. await addLog(`步骤 ${step}:提交失败后已请求新验证码(${attempt + 1}/${maxSubmitAttempts})...`, 'warn');
  422. continue;
  423. }
  424. await setState({
  425. lastEmailTimestamp: result.emailTimestamp,
  426. [stateKey]: result.code,
  427. });
  428. await completeStepFromBackground(step, {
  429. emailTimestamp: result.emailTimestamp,
  430. code: result.code,
  431. });
  432. return;
  433. }
  434. }
  435. return {
  436. confirmCustomVerificationStepBypass,
  437. getVerificationCodeLabel,
  438. getVerificationCodeStateKey,
  439. getVerificationPollPayload,
  440. pollFreshVerificationCode,
  441. pollFreshVerificationCodeWithResendInterval,
  442. requestVerificationCodeResend,
  443. resolveVerificationStep,
  444. submitVerificationCode,
  445. };
  446. }
  447. return {
  448. createVerificationFlowHelpers,
  449. };
  450. });