verification-flow.js 18 KB

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