diff --git a/background.js b/background.js index 231b633..7686a8a 100644 --- a/background.js +++ b/background.js @@ -5411,6 +5411,7 @@ const mail2925SessionManager = self.MultiPageBackgroundMail2925Session?.createMa normalizeMail2925Accounts, pickMail2925AccountForRun, requestStop, + ensureContentScriptReadyOnTab, reuseOrCreateTab, sendToContentScriptResilient, sendToMailContentScriptResilient, @@ -5419,6 +5420,7 @@ const mail2925SessionManager = self.MultiPageBackgroundMail2925Session?.createMa sleepWithStop, throwIfStopped, upsertMail2925AccountInList, + waitForTabUrlMatch, }); async function upsertMail2925Account(input = {}) { diff --git a/background/mail-2925-session.js b/background/mail-2925-session.js index 3bdc609..4649231 100644 --- a/background/mail-2925-session.js +++ b/background/mail-2925-session.js @@ -15,6 +15,7 @@ pickMail2925AccountForRun, getState, isAutoRunLockedState, + ensureContentScriptReadyOnTab, requestStop, reuseOrCreateTab, sendToContentScriptResilient, @@ -24,11 +25,12 @@ sleepWithStop, throwIfStopped, upsertMail2925AccountInList, + waitForTabUrlMatch, } = deps; const MAIL2925_SOURCE = 'mail-2925'; const MAIL2925_URL = 'https://2925.com/#/mailList'; - const MAIL2925_LOGIN_URL = 'https://2925.com/'; + const MAIL2925_LOGIN_URL = 'https://2925.com/login/'; const MAIL2925_INJECT = ['content/utils.js', 'content/mail-2925.js']; const MAIL2925_INJECT_SOURCE = 'mail-2925'; const MAIL2925_COOKIE_DOMAINS = [ @@ -387,7 +389,8 @@ } throwIfStopped(); - await reuseOrCreateTab(MAIL2925_SOURCE, forceRelogin ? MAIL2925_LOGIN_URL : MAIL2925_URL, { + const targetUrl = forceRelogin ? MAIL2925_LOGIN_URL : MAIL2925_URL; + const tabId = await reuseOrCreateTab(MAIL2925_SOURCE, targetUrl, { inject: MAIL2925_INJECT, injectSource: MAIL2925_INJECT_SOURCE, }); @@ -497,13 +500,40 @@ throwIfStopped(); await addLog(`2925:准备打开登录页 ${MAIL2925_LOGIN_URL}(forceRelogin=${forceRelogin ? 'true' : 'false'})`, 'info'); - await reuseOrCreateTab(MAIL2925_SOURCE, forceRelogin ? MAIL2925_LOGIN_URL : MAIL2925_URL, { + const targetUrl = forceRelogin ? MAIL2925_LOGIN_URL : MAIL2925_URL; + const tabId = await reuseOrCreateTab(MAIL2925_SOURCE, targetUrl, { inject: MAIL2925_INJECT, injectSource: MAIL2925_INJECT_SOURCE, }); const openedUrl = await getMail2925CurrentTabUrl(); await addLog(`2925:打开页后当前标签地址:${openedUrl || 'unknown'}`, 'info'); + if (forceRelogin && typeof waitForTabUrlMatch === 'function') { + const matchedLoginTab = await waitForTabUrlMatch( + tabId, + (url) => { + try { + const parsed = new URL(String(url || '')); + return (parsed.hostname === '2925.com' || parsed.hostname === 'www.2925.com') + && /^\/login\/?$/.test(parsed.pathname); + } catch { + return false; + } + }, + { timeoutMs: 15000, retryDelayMs: 300 } + ); + await addLog(`2925:等待最终落到登录页结果:${matchedLoginTab?.url || 'timeout'}`, matchedLoginTab ? 'info' : 'warn'); + if (matchedLoginTab && typeof ensureContentScriptReadyOnTab === 'function') { + await ensureContentScriptReadyOnTab(MAIL2925_SOURCE, tabId, { + inject: MAIL2925_INJECT, + injectSource: MAIL2925_INJECT_SOURCE, + timeoutMs: 20000, + retryDelayMs: 800, + logMessage: '步骤 0:2925 登录页内容脚本未就绪,正在等待页面稳定后继续登录...', + }); + } + } + if (forceRelogin && typeof sleepWithStop === 'function') { await addLog('2925:登录页已打开,等待 3 秒后开始检查输入框并执行登录...', 'info'); await sleepWithStop(3000); diff --git a/background/steps/fetch-login-code.js b/background/steps/fetch-login-code.js index 40dec9a..9e16ab6 100644 --- a/background/steps/fetch-login-code.js +++ b/background/steps/fetch-login-code.js @@ -1,6 +1,8 @@ (function attachBackgroundStep8(root, factory) { root.MultiPageBackgroundStep8 = factory(); })(typeof self !== 'undefined' ? self : globalThis, function createBackgroundStep8Module() { + const MAIL_2925_FILTER_LOOKBACK_MS = 10 * 60 * 1000; + function createStep8Executor(deps = {}) { const { addLog, @@ -61,6 +63,9 @@ if (mail.error) throw new Error(mail.error); const stepStartedAt = Date.now(); + const verificationFilterAfterTimestamp = mail.provider === '2925' + ? Math.max(0, stepStartedAt - MAIL_2925_FILTER_LOOKBACK_MS) + : stepStartedAt; const verificationSessionKey = `8:${stepStartedAt}`; const authTabId = await getTabId('signup-page'); @@ -140,7 +145,7 @@ ...state, step8VerificationTargetEmail: displayedVerificationEmail || '', }, mail, { - filterAfterTimestamp: mail.provider === '2925' ? 0 : stepStartedAt, + filterAfterTimestamp: verificationFilterAfterTimestamp, sessionKey: verificationSessionKey, disableTimeBudgetCap: mail.provider === '2925', getRemainingTimeMs: getStep8RemainingTimeResolver(state?.oauthUrl || ''), diff --git a/background/steps/fetch-signup-code.js b/background/steps/fetch-signup-code.js index fb5a1d6..e85b250 100644 --- a/background/steps/fetch-signup-code.js +++ b/background/steps/fetch-signup-code.js @@ -1,6 +1,8 @@ (function attachBackgroundStep4(root, factory) { root.MultiPageBackgroundStep4 = factory(); })(typeof self !== 'undefined' ? self : globalThis, function createBackgroundStep4Module() { + const MAIL_2925_FILTER_LOOKBACK_MS = 10 * 60 * 1000; + function createStep4Executor(deps = {}) { const { addLog, @@ -26,6 +28,9 @@ const mail = getMailConfig(state); if (mail.error) throw new Error(mail.error); const stepStartedAt = Date.now(); + const verificationFilterAfterTimestamp = mail.provider === '2925' + ? Math.max(0, stepStartedAt - MAIL_2925_FILTER_LOOKBACK_MS) + : stepStartedAt; const verificationSessionKey = `4:${stepStartedAt}`; const signupTabId = await getTabId('signup-page'); if (!signupTabId) { @@ -101,7 +106,7 @@ } await resolveVerificationStep(4, state, mail, { - filterAfterTimestamp: mail.provider === '2925' ? 0 : stepStartedAt, + filterAfterTimestamp: verificationFilterAfterTimestamp, sessionKey: verificationSessionKey, disableTimeBudgetCap: mail.provider === '2925', requestFreshCodeFirst: mail.provider === HOTMAIL_PROVIDER ? false : true, diff --git a/background/verification-flow.js b/background/verification-flow.js index db497a5..56a428e 100644 --- a/background/verification-flow.js +++ b/background/verification-flow.js @@ -239,12 +239,16 @@ return requestedAt; } - function shouldPreclear2925Mailbox(step, mail) { - return mail?.provider === '2925' && (step === 4 || step === 8); + function shouldPreclear2925Mailbox(step, mail, options = {}) { + if (mail?.provider !== '2925' || (step !== 4 && step !== 8)) { + return false; + } + + return !(Number(options.filterAfterTimestamp) > 0); } async function clear2925MailboxBeforePolling(step, mail, options = {}) { - if (!shouldPreclear2925Mailbox(step, mail)) { + if (!shouldPreclear2925Mailbox(step, mail, options)) { return; } diff --git a/content/mail-2925.js b/content/mail-2925.js index 4c036b1..54970d2 100644 --- a/content/mail-2925.js +++ b/content/mail-2925.js @@ -629,6 +629,13 @@ function extractVerificationCode(text, strictChatGPTCodeOnly = false) { return null; } +function normalizeMinuteTimestamp(timestamp) { + if (!Number.isFinite(timestamp) || timestamp <= 0) return 0; + const date = new Date(timestamp); + date.setSeconds(0, 0); + return date.getTime(); +} + function parseMailItemTimestamp(item) { const timeText = getMailItemTimeText(item); if (!timeText) return null; @@ -920,10 +927,12 @@ async function handlePollEmail(step, payload) { subjectFilters, maxAttempts, intervalMs, + filterAfterTimestamp = 0, excludeCodes = [], strictChatGPTCodeOnly = false, } = payload || {}; const excludedCodeSet = new Set(excludeCodes.filter(Boolean)); + const filterAfterMinute = normalizeMinuteTimestamp(Number(filterAfterTimestamp) || 0); if (typeof throwIfMail2925LimitReached === 'function') { throwIfMail2925LimitReached(); } @@ -975,6 +984,11 @@ async function handlePollEmail(step, payload) { for (let index = 0; index < items.length; index += 1) { const item = items[index]; const itemTimestamp = parseMailItemTimestamp(item); + const itemMinute = normalizeMinuteTimestamp(itemTimestamp || 0); + + if (filterAfterMinute && (!itemMinute || itemMinute < filterAfterMinute)) { + continue; + } const previewText = getMailItemText(item); if (!matchesMailFilters(previewText, senderFilters, subjectFilters)) { diff --git a/manifest.json b/manifest.json index 43834c0..c395652 100644 --- a/manifest.json +++ b/manifest.json @@ -1,8 +1,8 @@ { "manifest_version": 3, "name": "多页面自动化", - "version": "4.9", - "version_name": "Pro4.9", + "version": "5.0", + "version_name": "Pro5.0", "description": "用于自动执行多步骤 OAuth 注册流程", "permissions": [ "sidePanel", diff --git a/sidepanel/contribution-content-update-service.js b/sidepanel/contribution-content-update-service.js new file mode 100644 index 0000000..53db4e1 --- /dev/null +++ b/sidepanel/contribution-content-update-service.js @@ -0,0 +1,147 @@ +(() => { + const PORTAL_BASE_URL = 'https://apikey.qzz.io'; + const CONTENT_SUMMARY_API_URL = `${PORTAL_BASE_URL}/api/content-summary`; + const CACHE_KEY = 'multipage-contribution-content-summary-v1'; + const FETCH_TIMEOUT_MS = 6000; + + function sanitizeItem(item = {}) { + return { + slug: String(item?.slug || '').trim(), + title: String(item?.title || '').trim(), + isEnabled: Boolean(item?.is_enabled), + hasContent: Boolean(item?.has_content), + isVisible: Boolean(item?.is_visible), + updatedAt: String(item?.updated_at || '').trim(), + updatedAtDisplay: String(item?.updated_at_display || '').trim(), + }; + } + + function buildSnapshot(payload = {}) { + const items = Array.isArray(payload?.items) + ? payload.items.map(sanitizeItem).filter((item) => item.slug) + : []; + const promptVersion = String(payload?.prompt_version || '').trim(); + const latestUpdatedAt = String(payload?.latest_updated_at || '').trim(); + const latestUpdatedAtDisplay = String(payload?.latest_updated_at_display || '').trim(); + const hasVisibleUpdates = Boolean(payload?.has_visible_updates) && Boolean(promptVersion); + + return { + status: hasVisibleUpdates ? 'update-available' : 'idle', + promptVersion, + hasVisibleUpdates, + latestUpdatedAt, + latestUpdatedAtDisplay, + items, + portalUrl: PORTAL_BASE_URL, + apiUrl: CONTENT_SUMMARY_API_URL, + checkedAt: Date.now(), + }; + } + + function readCache() { + try { + const raw = localStorage.getItem(CACHE_KEY); + if (!raw) { + return null; + } + + const parsed = JSON.parse(raw); + if (!parsed || typeof parsed !== 'object') { + return null; + } + + const snapshot = buildSnapshot({ + items: parsed.items, + prompt_version: parsed.promptVersion, + has_visible_updates: parsed.hasVisibleUpdates, + latest_updated_at: parsed.latestUpdatedAt, + latest_updated_at_display: parsed.latestUpdatedAtDisplay, + }); + if (!Number.isFinite(parsed.checkedAt)) { + return snapshot; + } + snapshot.checkedAt = parsed.checkedAt; + return snapshot; + } catch (error) { + return null; + } + } + + function writeCache(snapshot) { + try { + localStorage.setItem(CACHE_KEY, JSON.stringify(snapshot)); + } catch (error) { + // Ignore cache write failures. + } + } + + async function fetchContentSummary() { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS); + + try { + const response = await fetch(CONTENT_SUMMARY_API_URL, { + method: 'GET', + headers: { + Accept: 'application/json', + }, + cache: 'no-store', + signal: controller.signal, + }); + + if (!response.ok) { + throw new Error(`内容摘要请求失败:${response.status}`); + } + + const payload = await response.json(); + if (!payload || payload.ok !== true) { + throw new Error('内容摘要返回格式异常'); + } + + const snapshot = buildSnapshot(payload); + writeCache(snapshot); + return snapshot; + } catch (error) { + if (error?.name === 'AbortError') { + throw new Error('内容摘要请求超时'); + } + throw error; + } finally { + clearTimeout(timeoutId); + } + } + + async function getContentUpdateSnapshot() { + try { + return await fetchContentSummary(); + } catch (error) { + const cached = readCache(); + if (cached) { + return { + ...cached, + fromCache: true, + errorMessage: error?.message || '内容摘要获取失败', + }; + } + + return { + status: 'error', + promptVersion: '', + hasVisibleUpdates: false, + latestUpdatedAt: '', + latestUpdatedAtDisplay: '', + items: [], + portalUrl: PORTAL_BASE_URL, + apiUrl: CONTENT_SUMMARY_API_URL, + checkedAt: Date.now(), + errorMessage: error?.message || '内容摘要获取失败', + }; + } + } + + window.SidepanelContributionContentService = { + getContentUpdateSnapshot, + portalUrl: PORTAL_BASE_URL, + apiUrl: CONTENT_SUMMARY_API_URL, + }; +})(); diff --git a/sidepanel/sidepanel.css b/sidepanel/sidepanel.css index 09e8a71..87558d9 100644 --- a/sidepanel/sidepanel.css +++ b/sidepanel/sidepanel.css @@ -243,6 +243,76 @@ header { color: var(--orange); } +.contribution-entry { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 6px; + position: relative; +} + +.contribution-update-hint { + position: relative; + display: flex; + align-items: flex-start; + gap: 8px; + max-width: 220px; + margin-left: 6px; + padding: 8px 10px; + background: color-mix(in srgb, var(--amber) 10%, var(--bg-base)); + border: 1px solid color-mix(in srgb, var(--amber) 34%, var(--border)); + border-radius: 10px; + box-shadow: var(--shadow-sm); +} + +.contribution-update-hint::before { + content: ''; + position: absolute; + top: -7px; + left: 14px; + width: 12px; + height: 12px; + background: inherit; + border-top: inherit; + border-left: inherit; + transform: rotate(45deg); +} + +.contribution-update-hint-text { + font-size: 12px; + line-height: 1.45; + color: var(--text-primary); +} + +.contribution-update-hint-close { + display: inline-flex; + align-items: center; + justify-content: center; + width: 20px; + height: 20px; + padding: 0; + border: none; + border-radius: 999px; + background: transparent; + color: var(--text-secondary); + font: inherit; + font-size: 14px; + line-height: 1; + cursor: pointer; + flex-shrink: 0; + transition: background var(--transition), color var(--transition); +} + +.contribution-update-hint-close:hover { + background: color-mix(in srgb, var(--amber) 16%, transparent); + color: var(--text-primary); +} + +.contribution-update-hint-close:focus-visible { + outline: 2px solid color-mix(in srgb, var(--amber) 34%, transparent); + outline-offset: 1px; +} + /* ============================================================ Theme Toggle ============================================================ */ @@ -366,7 +436,7 @@ header { .run-group { display: flex; - align-items: center; + align-items: flex-start; gap: 4px; flex-wrap: wrap; } diff --git a/sidepanel/sidepanel.html b/sidepanel/sidepanel.html index 33dfdae..0215ae8 100644 --- a/sidepanel/sidepanel.html +++ b/sidepanel/sidepanel.html @@ -34,8 +34,17 @@
- +
+ + +