From 344158e465c8a32a6a2f628567ceefde44fcf58c Mon Sep 17 00:00:00 2001 From: QLHazyCoder <2825305047@qq.com> Date: Sun, 26 Apr 2026 15:59:51 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=BC=BA=20Plus=20Checkout=20?= =?UTF-8?q?=E8=B4=A6=E5=8D=95=E5=9C=B0=E5=9D=80=E5=A4=84=E7=90=86=EF=BC=8C?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E5=A4=9A=E5=9B=BD=E5=9C=B0=E5=9D=80=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E5=8F=8A=E6=9C=AC=E5=9C=B0=E5=8C=96=E5=9B=BD=E5=AE=B6?= =?UTF-8?q?=E8=B7=AF=E5=BE=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- background/steps/fill-plus-checkout.js | 107 ++++++++++++++---- content/plus-checkout.js | 17 +++ ...us-checkout-billing-tab-resolution.test.js | 52 ++++++++- 3 files changed, 155 insertions(+), 21 deletions(-) diff --git a/background/steps/fill-plus-checkout.js b/background/steps/fill-plus-checkout.js index c02c58a..e182799 100644 --- a/background/steps/fill-plus-checkout.js +++ b/background/steps/fill-plus-checkout.js @@ -6,13 +6,30 @@ const PLUS_CHECKOUT_URL_PATTERN = /^https:\/\/chatgpt\.com\/checkout(?:\/|$)/i; const PLUS_CHECKOUT_FRAME_READY_DELAY_MS = 500; const MEIGUODIZHI_ADDRESS_ENDPOINT = 'https://www.meiguodizhi.com/api/v1/dz'; - const MEIGUODIZHI_PATH_BY_COUNTRY = { - AU: '/au-address', - DE: '/de-address', - FR: '/fr-address', - US: '/', + const MEIGUODIZHI_COUNTRY_CONFIG = { + AR: { path: '/ar-address', city: 'Buenos Aires', aliases: ['ar', 'argentina', '阿根廷'] }, + AU: { path: '/au-address', city: 'Sydney', aliases: ['au', 'aus', 'australia', '澳大利亚'] }, + CA: { path: '/ca-address', city: 'Toronto', aliases: ['ca', 'canada', '加拿大'] }, + CN: { path: '/cn-address', city: 'Shanghai', aliases: ['cn', 'china', '中国'] }, + DE: { path: '/de-address', city: 'Berlin', aliases: ['de', 'deu', 'germany', 'deutschland', '德国'] }, + ES: { path: '/es-address', city: 'Madrid', aliases: ['es', 'esp', 'spain', '西班牙'] }, + FR: { path: '/fr-address', city: 'Paris', aliases: ['fr', 'fra', 'france', '法国'] }, + GB: { path: '/uk-address', city: 'London', aliases: ['gb', 'uk', 'united kingdom', 'britain', 'england', '英国'] }, + HK: { path: '/hk-address', city: 'Hong Kong', aliases: ['hk', 'hong kong', '香港'] }, + IT: { path: '/it-address', city: 'Rome', aliases: ['it', 'ita', 'italy', '意大利'] }, + JP: { path: '/jp-address', city: 'Tokyo', aliases: ['jp', 'jpn', 'japan', '日本', '日本国'] }, + KR: { path: '/kr-address', city: 'Seoul', aliases: ['kr', 'kor', 'korea', 'south korea', '韩国'] }, + MY: { path: '/my-address', city: 'Kuala Lumpur', aliases: ['my', 'malaysia', '马来西亚'] }, + NL: { path: '/nl-address', city: 'Amsterdam', aliases: ['nl', 'netherlands', 'holland', '荷兰'] }, + PH: { path: '/ph-address', city: 'Manila', aliases: ['ph', 'philippines', '菲律宾'] }, + RU: { path: '/ru-address', city: 'Moscow', aliases: ['ru', 'russia', '俄罗斯'] }, + SG: { path: '/sg-address', city: 'Singapore', aliases: ['sg', 'singapore', '新加坡'] }, + TH: { path: '/th-address', city: 'Bangkok', aliases: ['th', 'thailand', '泰国'] }, + TR: { path: '/tr-address', city: 'Istanbul', aliases: ['tr', 'turkey', 'turkiye', '土耳其'] }, + TW: { path: '/tw-address', city: 'Taipei', aliases: ['tw', 'taiwan', '台湾'] }, + US: { path: '/', city: 'New York', aliases: ['us', 'usa', 'united states', 'united states of america', 'america', '美国'] }, + VN: { path: '/vn-address', city: 'Ho Chi Minh City', aliases: ['vn', 'vietnam', '越南'] }, }; - const MEIGUODIZHI_SUPPORTED_COUNTRIES = new Set(Object.keys(MEIGUODIZHI_PATH_BY_COUNTRY)); function createPlusCheckoutBillingExecutor(deps = {}) { const { @@ -39,6 +56,27 @@ return String(value || '').replace(/\s+/g, ' ').trim(); } + function compactCountryText(value = '') { + return normalizeText(value).toLowerCase().replace(/[^a-z0-9\u4e00-\u9fff]/g, ''); + } + + function resolveMeiguodizhiCountryCode(value = '') { + const normalized = normalizeText(value); + const upper = normalized.toUpperCase(); + if (MEIGUODIZHI_COUNTRY_CONFIG[upper]) { + return upper; + } + const compact = compactCountryText(normalized); + const match = Object.entries(MEIGUODIZHI_COUNTRY_CONFIG).find(([code, config]) => ( + compact === code.toLowerCase() + || (config.aliases || []).some((alias) => { + const compactAlias = compactCountryText(alias); + return compact === compactAlias || compact.includes(compactAlias); + }) + )); + return match?.[0] || ''; + } + function hasCompleteAddressFallback(seed) { const fallback = seed?.fallback || {}; return Boolean( @@ -49,9 +87,9 @@ } function buildDirectAddressSeed(countryCode, apiAddress, fallbackSeed) { - const address1 = normalizeText(apiAddress?.Address); + const address1 = normalizeText(apiAddress?.Trans_Address || apiAddress?.Address); const city = normalizeText(apiAddress?.City); - const region = normalizeText(apiAddress?.State || apiAddress?.State_Full); + const region = normalizeText(apiAddress?.State_Full || apiAddress?.State); const postalCode = normalizeText(apiAddress?.Zip_Code); if (!address1 || !city || !postalCode) { return null; @@ -76,11 +114,12 @@ if (typeof fetchImpl !== 'function') { return null; } - if (!MEIGUODIZHI_SUPPORTED_COUNTRIES.has(countryCode)) { + const countryConfig = MEIGUODIZHI_COUNTRY_CONFIG[countryCode]; + if (!countryConfig?.path) { return null; } - const path = MEIGUODIZHI_PATH_BY_COUNTRY[countryCode] || MEIGUODIZHI_PATH_BY_COUNTRY.DE; - const city = normalizeText(fallbackSeed?.fallback?.city || fallbackSeed?.query || ''); + const path = countryConfig.path; + const city = normalizeText(fallbackSeed?.fallback?.city || fallbackSeed?.query || countryConfig.city); const response = await fetchImpl(MEIGUODIZHI_ADDRESS_ENDPOINT, { method: 'POST', headers: { @@ -102,18 +141,43 @@ return buildDirectAddressSeed(countryCode, data.address || {}, fallbackSeed); } - async function resolveBillingAddressSeed(state = {}, countryOverride = '') { - const requestedCountry = normalizeText(countryOverride || state.plusCheckoutCountry || 'DE'); - const fallbackSeed = getAddressSeedForCountry(requestedCountry, { + function getLocalAddressSeed(countryCode) { + if (typeof getAddressSeedForCountry !== 'function') { + return null; + } + const seed = getAddressSeedForCountry(countryCode, { fallbackCountry: 'DE', }); - if (!fallbackSeed) { - throw new Error('步骤 7:未找到可用的本地账单地址种子。'); - } + return seed?.countryCode === countryCode ? seed : null; + } - const countryCode = fallbackSeed.countryCode || 'DE'; + function buildMeiguodizhiLookupSeed(countryCode) { + const config = MEIGUODIZHI_COUNTRY_CONFIG[countryCode]; + if (!config) { + return null; + } + return { + countryCode, + query: config.city, + fallback: { + address1: '', + city: config.city, + region: '', + postalCode: '', + }, + }; + } + + async function resolveBillingAddressSeed(state = {}, countryOverride = '') { + const requestedCountry = normalizeText(countryOverride || state.plusCheckoutCountry || 'DE'); + const countryCode = resolveMeiguodizhiCountryCode(requestedCountry) || 'DE'; + const localSeed = getLocalAddressSeed(countryCode); + const lookupSeed = localSeed || buildMeiguodizhiLookupSeed(countryCode); + if (!lookupSeed) { + throw new Error(`步骤 7:无法识别账单国家或地区:${requestedCountry || '空'}`); + } try { - const remoteSeed = await fetchMeiguodizhiAddressSeed(countryCode, fallbackSeed); + const remoteSeed = await fetchMeiguodizhiAddressSeed(countryCode, lookupSeed); if (hasCompleteAddressFallback(remoteSeed)) { await addLog( `步骤 7:已从 meiguodizhi 接口获取账单地址(${remoteSeed.fallback.city} / ${remoteSeed.fallback.postalCode}),将跳过 Google 地址推荐。`, @@ -126,7 +190,10 @@ await addLog(`步骤 7:meiguodizhi 地址接口不可用,回退到本地地址种子:${error?.message || String(error || '')}`, 'warn'); } - return fallbackSeed; + if (hasCompleteAddressFallback(localSeed)) { + return localSeed; + } + throw new Error(`步骤 7:${requestedCountry} 的 meiguodizhi 地址不可用,且没有本地兜底地址。`); } async function getAlivePlusCheckoutTabId(tabId) { diff --git a/content/plus-checkout.js b/content/plus-checkout.js index 6ba1026..81a10ad 100644 --- a/content/plus-checkout.js +++ b/content/plus-checkout.js @@ -689,11 +689,28 @@ function getCountryCandidates(value = '') { const raw = normalizeText(value); const compact = raw.toLowerCase().replace(/[^a-z0-9\u4e00-\u9fff]/g, ''); const aliases = { + AR: ['Argentina', '阿根廷'], AU: ['Australia', '澳大利亚'], + CA: ['Canada', '加拿大'], + CN: ['China', '中国'], DE: ['Germany', 'Deutschland', '德国'], + ES: ['Spain', '西班牙'], FR: ['France', '法国'], + GB: ['United Kingdom', 'UK', 'Britain', 'England', '英国'], + HK: ['Hong Kong', '香港'], + IT: ['Italy', '意大利'], JP: ['Japan', '日本', '日本国'], + KR: ['Korea', 'South Korea', '韩国'], + MY: ['Malaysia', '马来西亚'], + NL: ['Netherlands', 'Holland', '荷兰'], + PH: ['Philippines', '菲律宾'], + RU: ['Russia', '俄罗斯'], + SG: ['Singapore', '新加坡'], + TH: ['Thailand', '泰国'], + TR: ['Turkey', 'Turkiye', '土耳其'], + TW: ['Taiwan', '台湾'], US: ['United States', 'United States of America', 'USA', '美国'], + VN: ['Vietnam', '越南'], }; const direct = aliases[String(raw || '').trim().toUpperCase()] || []; const matched = Object.entries(aliases).find(([code, names]) => { diff --git a/tests/plus-checkout-billing-tab-resolution.test.js b/tests/plus-checkout-billing-tab-resolution.test.js index 350d6f5..24abcc4 100644 --- a/tests/plus-checkout-billing-tab-resolution.test.js +++ b/tests/plus-checkout-billing-tab-resolution.test.js @@ -344,7 +344,7 @@ test('Plus checkout billing uses the detected checkout country before choosing a await executor.executePlusCheckoutBilling({ plusCheckoutCountry: 'DE' }); const combinedFillMessage = events.messages.find((entry) => entry.message.type === 'PLUS_CHECKOUT_FILL_BILLING_ADDRESS'); - assert.equal(requestedCountries[0], 'Australia'); + assert.equal(requestedCountries[0], 'AU'); assert.equal(combinedFillMessage.message.payload.addressSeed.countryCode, 'AU'); assert.equal(combinedFillMessage.message.payload.addressSeed.fallback.region, 'New South Wales'); assert.deepEqual(JSON.parse(fetchRequests[0].init.body), { @@ -354,6 +354,56 @@ test('Plus checkout billing uses the detected checkout country before choosing a }); }); +test('Plus checkout billing uses meiguodizhi country paths for localized countries without local seeds', async () => { + const fetchRequests = []; + const { events, executor } = createExecutorHarness({ + frames: [ + { frameId: 0, url: 'https://chatgpt.com/checkout/openai_ie/cs_test' }, + { frameId: 7, url: 'https://js.stripe.com/v3/elements-inner-payment.html' }, + { frameId: 8, url: 'https://js.stripe.com/v3/elements-inner-address.html' }, + ], + stateByFrame: { + 0: { hasPayPal: false, paypalCandidates: [], hasSubscribeButton: true }, + 7: { hasPayPal: true, paypalCandidates: [{ tag: 'button', text: 'PayPal' }] }, + 8: { + hasPayPal: false, + paypalCandidates: [], + billingFieldsVisible: true, + countryText: '日本', + }, + }, + fetchImpl: async (url, init) => { + fetchRequests.push({ url, init }); + return { + ok: true, + status: 200, + json: async () => ({ + status: 'ok', + address: { + Address: 'トウキョウト, ミナトク, シバダイモン, 10-4', + Trans_Address: '10-4, Shiba Daimon 2-chome, Minato-ku, Tokyo', + City: 'Tokyo', + State: 'Tokyo', + Zip_Code: '105-0012', + }, + }), + }; + }, + }); + + await executor.executePlusCheckoutBilling({ plusCheckoutCountry: 'DE' }); + + const combinedFillMessage = events.messages.find((entry) => entry.message.type === 'PLUS_CHECKOUT_FILL_BILLING_ADDRESS'); + assert.equal(combinedFillMessage.message.payload.addressSeed.countryCode, 'JP'); + assert.equal(combinedFillMessage.message.payload.addressSeed.source, 'meiguodizhi'); + assert.equal(combinedFillMessage.message.payload.addressSeed.fallback.address1, '10-4, Shiba Daimon 2-chome, Minato-ku, Tokyo'); + assert.deepEqual(JSON.parse(fetchRequests[0].init.body), { + city: 'Tokyo', + path: '/jp-address', + method: 'refresh', + }); +}); + test('Plus checkout billing reports when the payment iframe exists but cannot receive the content script', async () => { const { executor } = createExecutorHarness({ frames: [