(step, payload)
| 55 | // ============================================================ |
| 56 | |
| 57 | async function handlePollEmail(step, payload) { |
| 58 | const { senderFilters, subjectFilters, maxAttempts, intervalMs, excludeCodes = [] } = payload; |
| 59 | const excludedCodeSet = new Set(excludeCodes.filter(Boolean)); |
| 60 | |
| 61 | log(`步骤 ${step}:开始轮询邮箱(最多 ${maxAttempts} 次,每 ${intervalMs / 1000} 秒一次)`); |
| 62 | |
| 63 | // Wait for mail list to load |
| 64 | try { |
| 65 | await waitForElement('.mail-list-page-item', 10000); |
| 66 | log(`步骤 ${step}:邮件列表已加载`); |
| 67 | } catch { |
| 68 | throw new Error('邮件列表未加载完成,请确认 QQ 邮箱已打开收件箱。'); |
| 69 | } |
| 70 | |
| 71 | // Step 1: Snapshot existing mail IDs BEFORE we start waiting for new email |
| 72 | const existingMailIds = getCurrentMailIds(); |
| 73 | log(`步骤 ${step}:已将当前 ${existingMailIds.size} 封邮件标记为旧邮件快照`); |
| 74 | |
| 75 | // Fallback after just 3 attempts (~10s). In practice, the email is usually |
| 76 | // already in the list but has the same mailid (page was already open). |
| 77 | const FALLBACK_AFTER = 3; |
| 78 | |
| 79 | for (let attempt = 1; attempt <= maxAttempts; attempt++) { |
| 80 | log(`步骤 ${step}:正在轮询 QQ 邮箱,第 ${attempt}/${maxAttempts} 次`); |
| 81 | |
| 82 | // Refresh inbox (skip on first attempt, list is fresh) |
| 83 | if (attempt > 1) { |
| 84 | await refreshInbox(); |
| 85 | await sleep(800); |
| 86 | } |
| 87 | |
| 88 | const allItems = document.querySelectorAll('.mail-list-page-item[data-mailid]'); |
| 89 | const useFallback = attempt > FALLBACK_AFTER; |
| 90 | |
| 91 | // Phase 1 (attempt 1~3): only look at NEW emails (not in snapshot) |
| 92 | // Phase 2 (attempt 4+): fallback to first matching email in list |
| 93 | for (const item of allItems) { |
| 94 | const mailId = item.getAttribute('data-mailid'); |
| 95 | |
| 96 | if (!useFallback && existingMailIds.has(mailId)) continue; |
| 97 | |
| 98 | const sender = (item.querySelector('.cmp-account-nick')?.textContent || '').toLowerCase(); |
| 99 | const subject = (item.querySelector('.mail-subject')?.textContent || '').toLowerCase(); |
| 100 | const digest = item.querySelector('.mail-digest')?.textContent || ''; |
| 101 | |
| 102 | const senderMatch = senderFilters.some(f => sender.includes(f.toLowerCase())); |
| 103 | const subjectMatch = subjectFilters.some(f => subject.includes(f.toLowerCase())); |
| 104 | |
| 105 | if (senderMatch || subjectMatch) { |
| 106 | const code = extractVerificationCode(subject + ' ' + digest); |
| 107 | if (code) { |
| 108 | if (excludedCodeSet.has(code)) { |
| 109 | log(`步骤 ${step}:跳过排除的验证码:${code}`, 'info'); |
| 110 | continue; |
| 111 | } |
| 112 | const source = useFallback && existingMailIds.has(mailId) ? '回退首封匹配邮件' : '新邮件'; |
| 113 | log(`步骤 ${step}:已找到验证码:${code}(来源:${source},主题:${subject.slice(0, 40)})`, 'ok'); |
| 114 | return { ok: true, code, emailTimestamp: Date.now(), mailId }; |
no test coverage detected