From 336fdb0c3aca85b5e9b3ee23a59a0854d3fc6e82 Mon Sep 17 00:00:00 2001 From: QLHazyCoder <2825305047@qq.com> Date: Sun, 26 Apr 2026 15:29:26 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=BC=BA=E5=86=85=E5=AE=B9?= =?UTF-8?q?=E8=84=9A=E6=9C=AC=E5=8A=A0=E8=BD=BD=E9=80=BB=E8=BE=91=EF=BC=8C?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E9=94=99=E8=AF=AF=E5=A4=84=E7=90=86=E5=92=8C?= =?UTF-8?q?=E6=B5=8B=E8=AF=95=E7=94=A8=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- background.js | 20 +- background/steps/create-plus-checkout.js | 2 - content/plus-checkout.js | 1 + content/utils.js | 17 +- mail-provider-utils.js | 240 ++++++++++++----------- tests/content-utils.test.js | 13 ++ tests/plus-checkout-create-wait.test.js | 10 +- 7 files changed, 174 insertions(+), 129 deletions(-) diff --git a/background.js b/background.js index 8190aae..94e90f2 100644 --- a/background.js +++ b/background.js @@ -4122,6 +4122,19 @@ async function ensureContentScriptReadyOnTab(source, tabId, options = {}) { return tabRuntime.ensureContentScriptReadyOnTab(source, tabId, options); } +function isContentScriptReadyPong(source, pong) { + if (!pong?.ok) return false; + if (pong.source && pong.source !== source) return false; + if (source === 'plus-checkout') { + return Boolean(pong.plusCheckoutReady); + } + return true; +} + +function isUnrecoverableContentScriptInjectError(error) { + return /Could not load file/i.test(String(error?.message || error || '')); +} + async function ensureContentScriptReadyOnTabUntilStopped(source, tabId, options = {}) { const { inject = null, @@ -4134,7 +4147,7 @@ async function ensureContentScriptReadyOnTabUntilStopped(source, tabId, options while (true) { throwIfStopped(); const pong = await pingContentScriptOnTab(tabId); - if (pong?.ok && (!pong.source || pong.source === source)) { + if (isContentScriptReadyPong(source, pong)) { await registerTab(source, tabId); return; } @@ -4159,10 +4172,13 @@ async function ensureContentScriptReadyOnTabUntilStopped(source, tabId, options }); } catch (error) { console.warn(LOG_PREFIX, `[ensureContentScriptReadyOnTabUntilStopped] inject failed for ${source}:`, error?.message || error); + if (isUnrecoverableContentScriptInjectError(error)) { + throw new Error(`${getSourceLabel(source)} 内容脚本文件加载失败:${error?.message || error}。请在扩展管理页重新加载当前扩展,确认文件已包含在已加载的扩展目录中。`); + } } const pongAfterInject = await pingContentScriptOnTab(tabId); - if (pongAfterInject?.ok && (!pongAfterInject.source || pongAfterInject.source === source)) { + if (isContentScriptReadyPong(source, pongAfterInject)) { await registerTab(source, tabId); return; } diff --git a/background/steps/create-plus-checkout.js b/background/steps/create-plus-checkout.js index 485aa94..ba8ca83 100644 --- a/background/steps/create-plus-checkout.js +++ b/background/steps/create-plus-checkout.js @@ -21,8 +21,6 @@ async function executePlusCheckoutCreate() { await addLog('步骤 6:正在打开 ChatGPT 会话页,准备创建 Plus Checkout...', 'info'); const tabId = await reuseOrCreateTab(PLUS_CHECKOUT_SOURCE, PLUS_CHECKOUT_ENTRY_URL, { - inject: PLUS_CHECKOUT_INJECT_FILES, - injectSource: PLUS_CHECKOUT_SOURCE, reloadIfSameUrl: false, }); diff --git a/content/plus-checkout.js b/content/plus-checkout.js index 1beca2d..749a733 100644 --- a/content/plus-checkout.js +++ b/content/plus-checkout.js @@ -1,6 +1,7 @@ // content/plus-checkout.js — ChatGPT Plus checkout helper. console.log('[MultiPage:plus-checkout] Content script loaded on', location.href); +window.__MULTIPAGE_PLUS_CHECKOUT_READY__ = true; const PLUS_CHECKOUT_LISTENER_SENTINEL = 'data-multipage-plus-checkout-listener'; const PLUS_CHECKOUT_PAYLOAD = { diff --git a/content/utils.js b/content/utils.js index a38143d..f208aca 100644 --- a/content/utils.js +++ b/content/utils.js @@ -34,6 +34,10 @@ const SCRIPT_SOURCE = (() => { }); })(); +function getRuntimeScriptSource() { + return window.__MULTIPAGE_SOURCE || SCRIPT_SOURCE; +} + const LOG_PREFIX = `[MultiPage:${SCRIPT_SOURCE}]`; const STOP_ERROR_MESSAGE = '流程已被用户停止。'; let flowStopped = false; @@ -51,7 +55,8 @@ if (!window.__MULTIPAGE_UTILS_LISTENER_READY__) { if (message.type === 'PING') { sendResponse({ ok: true, - source: SCRIPT_SOURCE, + source: getRuntimeScriptSource(), + plusCheckoutReady: Boolean(window.__MULTIPAGE_PLUS_CHECKOUT_READY__), }); } }); @@ -265,7 +270,7 @@ function fillSelect(el, value) { function log(message, level = 'info') { chrome.runtime.sendMessage({ type: 'LOG', - source: SCRIPT_SOURCE, + source: getRuntimeScriptSource(), step: null, payload: { message, level, timestamp: Date.now() }, error: null, @@ -279,7 +284,7 @@ function reportReady() { console.log(LOG_PREFIX, '内容脚本已就绪'); const message = { type: 'CONTENT_SCRIPT_READY', - source: SCRIPT_SOURCE, + source: getRuntimeScriptSource(), step: null, payload: {}, error: null, @@ -303,7 +308,7 @@ function reportComplete(step, data = {}) { log(`步骤 ${step} 已成功完成`, 'ok'); const message = { type: 'STEP_COMPLETE', - source: SCRIPT_SOURCE, + source: getRuntimeScriptSource(), step, payload: data, error: null, @@ -334,7 +339,7 @@ function reportError(step, errorMessage) { log(`步骤 ${step} 失败:${errorMessage}`, 'error'); const message = { type: 'STEP_ERROR', - source: SCRIPT_SOURCE, + source: getRuntimeScriptSource(), step, payload: {}, error: errorMessage, @@ -435,6 +440,6 @@ function shouldReportReadyForFrame(source, isChildFrame) { // Auto-report ready on load. Child frames are probed explicitly by frameId, so // they should not overwrite the tab-level registration or spam the side panel. -if (shouldReportReadyForFrame(SCRIPT_SOURCE, window !== window.top)) { +if (shouldReportReadyForFrame(getRuntimeScriptSource(), window !== window.top)) { reportReady(); } diff --git a/mail-provider-utils.js b/mail-provider-utils.js index 6bd3960..0a61579 100644 --- a/mail-provider-utils.js +++ b/mail-provider-utils.js @@ -1,127 +1,131 @@ -const HOTMAIL_PROVIDER = 'hotmail-api'; -const GMAIL_PROVIDER = 'gmail'; -const NETEASE_LIST_PATH = '/js6/main.jsp?df=mail163_letter#module=mbox.ListModule%7C%7B%22fid%22%3A1%2C%22order%22%3A%22date%22%2C%22desc%22%3Atrue%7D'; -const ICLOUD_TARGET_MAILBOX_TYPE_INBOX = 'icloud-inbox'; -const ICLOUD_TARGET_MAILBOX_TYPE_FORWARD = 'forward-mailbox'; -const ICLOUD_FORWARD_MAIL_PROVIDER_OPTIONS = [ - { value: 'qq', label: 'QQ 邮箱' }, - { value: '163', label: '163 邮箱' }, - { value: '163-vip', label: '163 VIP 邮箱' }, - { value: '126', label: '126 邮箱' }, - { value: GMAIL_PROVIDER, label: 'Gmail 邮箱' }, -]; +(function attachMailProviderUtils(root, factory) { + const api = factory(); -function normalizeMailProvider(value = '') { - const normalized = String(value || '').trim().toLowerCase(); - switch (normalized) { - case HOTMAIL_PROVIDER: - case '163': - case '163-vip': - case '126': - case 'qq': - case 'inbucket': - return normalized; - default: - return '163'; - } -} - -function normalizeIcloudTargetMailboxType(value = '') { - return String(value || '').trim().toLowerCase() === ICLOUD_TARGET_MAILBOX_TYPE_FORWARD - ? ICLOUD_TARGET_MAILBOX_TYPE_FORWARD - : ICLOUD_TARGET_MAILBOX_TYPE_INBOX; -} - -function normalizeIcloudForwardMailProvider(value = '') { - const normalized = String(value || '').trim().toLowerCase(); - return ICLOUD_FORWARD_MAIL_PROVIDER_OPTIONS.some((option) => option.value === normalized) - ? normalized - : 'qq'; -} - -function getIcloudForwardMailProviderOptions() { - return ICLOUD_FORWARD_MAIL_PROVIDER_OPTIONS.map((option) => ({ ...option })); -} - -function getIcloudForwardMailConfig(provider = 'qq') { - const normalizedProvider = normalizeIcloudForwardMailProvider(provider); - if (normalizedProvider === GMAIL_PROVIDER) { - return { - source: 'gmail-mail', - url: 'https://mail.google.com/mail/u/0/#inbox', - label: 'Gmail 邮箱', - inject: ['content/activation-utils.js', 'content/utils.js', 'content/gmail-mail.js'], - injectSource: 'gmail-mail', - }; + if (typeof module !== 'undefined' && module.exports) { + module.exports = api; } - return getMailProviderConfig({ mailProvider: normalizedProvider }); -} + if (root) { + root.MailProviderUtils = api; + } +})(typeof self !== 'undefined' ? self : globalThis, function createMailProviderUtils() { + const HOTMAIL_PROVIDER = 'hotmail-api'; + const GMAIL_PROVIDER = 'gmail'; + const NETEASE_LIST_PATH = '/js6/main.jsp?df=mail163_letter#module=mbox.ListModule%7C%7B%22fid%22%3A1%2C%22order%22%3A%22date%22%2C%22desc%22%3Atrue%7D'; + const ICLOUD_TARGET_MAILBOX_TYPE_INBOX = 'icloud-inbox'; + const ICLOUD_TARGET_MAILBOX_TYPE_FORWARD = 'forward-mailbox'; + const ICLOUD_FORWARD_MAIL_PROVIDER_OPTIONS = [ + { value: 'qq', label: 'QQ 邮箱' }, + { value: '163', label: '163 邮箱' }, + { value: '163-vip', label: '163 VIP 邮箱' }, + { value: '126', label: '126 邮箱' }, + { value: GMAIL_PROVIDER, label: 'Gmail 邮箱' }, + ]; -function getMailProviderConfig(state = {}, options = {}) { - const provider = normalizeMailProvider(state.mailProvider); - const normalizeInbucketOrigin = options.normalizeInbucketOrigin || (() => ''); - - if (provider === HOTMAIL_PROVIDER) { - return { provider: HOTMAIL_PROVIDER, label: 'Hotmail(微软 Graph)' }; - } - if (provider === '163') { - return { - source: 'mail-163', - url: `https://mail.163.com${NETEASE_LIST_PATH}`, - label: '163 邮箱', - }; - } - if (provider === '163-vip') { - return { - source: 'mail-163', - url: `https://webmail.vip.163.com${NETEASE_LIST_PATH}`, - label: '163 VIP 邮箱', - }; - } - if (provider === '126') { - return { - source: 'mail-163', - url: `https://mail.126.com${NETEASE_LIST_PATH}`, - label: '126 邮箱', - }; - } - if (provider === 'inbucket') { - const host = normalizeInbucketOrigin(state.inbucketHost); - const mailbox = String(state.inbucketMailbox || '').trim(); - if (!host) { - return { error: 'Inbucket 主机地址为空或无效。' }; + function normalizeMailProvider(value = '') { + const normalized = String(value || '').trim().toLowerCase(); + switch (normalized) { + case HOTMAIL_PROVIDER: + case '163': + case '163-vip': + case '126': + case 'qq': + case 'inbucket': + return normalized; + default: + return '163'; } - if (!mailbox) { - return { error: 'Inbucket 邮箱名称为空。' }; - } - return { - source: 'inbucket-mail', - url: `${host}/m/${encodeURIComponent(mailbox)}/`, - label: `Inbucket 邮箱(${mailbox})`, - navigateOnReuse: true, - inject: ['content/activation-utils.js', 'content/utils.js', 'content/inbucket-mail.js'], - injectSource: 'inbucket-mail', - }; } - return { source: 'qq-mail', url: 'https://wx.mail.qq.com/', label: 'QQ 邮箱' }; -} -const api = { - GMAIL_PROVIDER, - HOTMAIL_PROVIDER, - getIcloudForwardMailConfig, - getIcloudForwardMailProviderOptions, - getMailProviderConfig, - normalizeIcloudForwardMailProvider, - normalizeIcloudTargetMailboxType, - normalizeMailProvider, -}; + function normalizeIcloudTargetMailboxType(value = '') { + return String(value || '').trim().toLowerCase() === ICLOUD_TARGET_MAILBOX_TYPE_FORWARD + ? ICLOUD_TARGET_MAILBOX_TYPE_FORWARD + : ICLOUD_TARGET_MAILBOX_TYPE_INBOX; + } -if (typeof module !== 'undefined' && module.exports) { - module.exports = api; -} + function normalizeIcloudForwardMailProvider(value = '') { + const normalized = String(value || '').trim().toLowerCase(); + return ICLOUD_FORWARD_MAIL_PROVIDER_OPTIONS.some((option) => option.value === normalized) + ? normalized + : 'qq'; + } -if (typeof self !== 'undefined') { - self.MailProviderUtils = api; -} + function getIcloudForwardMailProviderOptions() { + return ICLOUD_FORWARD_MAIL_PROVIDER_OPTIONS.map((option) => ({ ...option })); + } + + function getIcloudForwardMailConfig(provider = 'qq') { + const normalizedProvider = normalizeIcloudForwardMailProvider(provider); + if (normalizedProvider === GMAIL_PROVIDER) { + return { + source: 'gmail-mail', + url: 'https://mail.google.com/mail/u/0/#inbox', + label: 'Gmail 邮箱', + inject: ['content/activation-utils.js', 'content/utils.js', 'content/gmail-mail.js'], + injectSource: 'gmail-mail', + }; + } + + return getMailProviderConfig({ mailProvider: normalizedProvider }); + } + + function getMailProviderConfig(state = {}, options = {}) { + const provider = normalizeMailProvider(state.mailProvider); + const normalizeInbucketOrigin = options.normalizeInbucketOrigin || (() => ''); + + if (provider === HOTMAIL_PROVIDER) { + return { provider: HOTMAIL_PROVIDER, label: 'Hotmail(微软 Graph)' }; + } + if (provider === '163') { + return { + source: 'mail-163', + url: `https://mail.163.com${NETEASE_LIST_PATH}`, + label: '163 邮箱', + }; + } + if (provider === '163-vip') { + return { + source: 'mail-163', + url: `https://webmail.vip.163.com${NETEASE_LIST_PATH}`, + label: '163 VIP 邮箱', + }; + } + if (provider === '126') { + return { + source: 'mail-163', + url: `https://mail.126.com${NETEASE_LIST_PATH}`, + label: '126 邮箱', + }; + } + if (provider === 'inbucket') { + const host = normalizeInbucketOrigin(state.inbucketHost); + const mailbox = String(state.inbucketMailbox || '').trim(); + if (!host) { + return { error: 'Inbucket 主机地址为空或无效。' }; + } + if (!mailbox) { + return { error: 'Inbucket 邮箱名称为空。' }; + } + return { + source: 'inbucket-mail', + url: `${host}/m/${encodeURIComponent(mailbox)}/`, + label: `Inbucket 邮箱(${mailbox})`, + navigateOnReuse: true, + inject: ['content/activation-utils.js', 'content/utils.js', 'content/inbucket-mail.js'], + injectSource: 'inbucket-mail', + }; + } + return { source: 'qq-mail', url: 'https://wx.mail.qq.com/', label: 'QQ 邮箱' }; + } + + return { + GMAIL_PROVIDER, + HOTMAIL_PROVIDER, + getIcloudForwardMailConfig, + getIcloudForwardMailProviderOptions, + getMailProviderConfig, + normalizeIcloudForwardMailProvider, + normalizeIcloudTargetMailboxType, + normalizeMailProvider, + }; +}); diff --git a/tests/content-utils.test.js b/tests/content-utils.test.js index 01380a5..8703fdd 100644 --- a/tests/content-utils.test.js +++ b/tests/content-utils.test.js @@ -92,3 +92,16 @@ return { shouldReportReadyForFrame }; assert.equal(api.shouldReportReadyForFrame('plus-checkout', false), true); assert.equal(api.shouldReportReadyForFrame('paypal-flow', true), true); }); + +test('getRuntimeScriptSource follows injected source overrides after utils is already loaded', () => { + const bundle = [extractFunction('getRuntimeScriptSource')].join('\n'); + const api = new Function('window', 'SCRIPT_SOURCE', ` +${bundle} +return { getRuntimeScriptSource }; +`); + + const windowRef = {}; + assert.equal(api(windowRef, 'chatgpt').getRuntimeScriptSource(), 'chatgpt'); + windowRef.__MULTIPAGE_SOURCE = 'plus-checkout'; + assert.equal(api(windowRef, 'chatgpt').getRuntimeScriptSource(), 'plus-checkout'); +}); diff --git a/tests/plus-checkout-create-wait.test.js b/tests/plus-checkout-create-wait.test.js index 3fd9f41..097c645 100644 --- a/tests/plus-checkout-create-wait.test.js +++ b/tests/plus-checkout-create-wait.test.js @@ -25,7 +25,10 @@ test('Plus checkout create does not wait 20 seconds after opening checkout page' ensureContentScriptReadyOnTabUntilStopped: async () => { events.push({ type: 'ready' }); }, - reuseOrCreateTab: async () => 42, + reuseOrCreateTab: async (source, url, options) => { + events.push({ type: 'reuse-tab', source, url, options }); + return 42; + }, sendTabMessageUntilStopped: async () => ({ checkoutUrl: 'https://checkout.stripe.com/c/pay/session', country: 'US', @@ -44,6 +47,11 @@ test('Plus checkout create does not wait 20 seconds after opening checkout page' await executor.executePlusCheckoutCreate(); + const reuseEvent = events.find((event) => event.type === 'reuse-tab'); + assert.equal(reuseEvent.source, 'plus-checkout'); + assert.equal(reuseEvent.options.reloadIfSameUrl, false); + assert.equal(Object.hasOwn(reuseEvent.options, 'inject'), false); + const sleepEvents = events.filter((event) => event.type === 'sleep'); assert.deepStrictEqual(sleepEvents.map((event) => event.ms), [1000, 1000]);