verification-flow.js 18 KB

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