// content/checkout-stripe.js — Stripe checkout page automation for ChatGPT Plus subscription (function attachCheckoutStripe() { if (document.documentElement.hasAttribute('data-multipage-checkout-stripe-listener')) return; document.documentElement.setAttribute('data-multipage-checkout-stripe-listener', ''); chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { if (message.type === 'EXECUTE_STEP' && message.step === 7) { resetStopState(); runStripeCheckout(message.payload || {}).then( (result) => sendResponse(result), (err) => sendResponse({ error: err.message }) ); return true; } if (message.type === 'CHECKOUT_STRIPE_SELECT_ADDRESS_SUGGESTION') { resetStopState(); selectGoogleAddressSuggestionOnly(message.payload || {}).then( (result) => sendResponse(result), (err) => sendResponse({ error: err.message }) ); return true; } if (message.type === 'CHECKOUT_STRIPE_GET_STATE') { sendResponse(inspectCheckoutStripeState()); return false; } if (message.type === 'CHECKOUT_STRIPE_SELECT_PAYPAL') { resetStopState(); selectPayPalPaymentMethod(message.payload || {}).then( (result) => sendResponse(result), (err) => sendResponse({ error: err.message }) ); return true; } if (message.type === 'CHECKOUT_STRIPE_FILL_BILLING_ADDRESS') { resetStopState(); fillStripeBillingAddress(message.payload || {}).then( (result) => sendResponse(result), (err) => sendResponse({ error: err.message }) ); return true; } if (message.type === 'CHECKOUT_STRIPE_CLICK_SUBMIT') { resetStopState(); clickStripeCheckoutSubmit(message.payload || {}).then( (result) => sendResponse(result), (err) => sendResponse({ error: err.message }) ); return true; } }); async function runStripeCheckout(payload) { try { throwIfStopped(); log('开始执行 Stripe 结账页面自动化...'); await sleep(1800); // 1. Click PayPal option await selectPayPalPaymentMethod({ relaxedActivation: true }); await sleep(1000); // 2. Fill billing address await fillStripeBillingAddress(payload); // 3. Click submit await clickStripeCheckoutSubmit({ beforeClickDelayMs: 1500 }); log('Stripe 结账表单已提交'); reportComplete(7, {}); return { ok: true }; } catch (e) { if (isStopError(e)) throw e; log('Stripe 结账流程出错: ' + e.message, 'error'); reportError(7, e.message); return { error: e.message }; } } function inspectCheckoutStripeState() { const addressFields = getStructuredAddressFields(); return { url: location.href, readyState: document.readyState, hasPayPal: Boolean(findPayPalPaymentMethodTarget()), paypalCandidates: getPayPalCandidateSummaries(), billingFieldsVisible: hasBillingAddressFields(addressFields), hasSubmitButton: Boolean(findSubmitButton()), addressFieldValues: { address1: addressFields.address1?.value || '', city: addressFields.city?.value || '', region: addressFields.region?.value || getSelectText(addressFields.regionSelect) || '', postalCode: addressFields.postalCode?.value || '', }, }; } async function selectPayPalPaymentMethod(options = {}) { log('正在寻找 PayPal 选项...'); const autoJsTarget = findAutoJsPayPalTarget(); if (autoJsTarget) { await clickPayPalLikeAutoJs(autoJsTarget); await sleep(450); await clickPayPalLikeAutoJs(autoJsTarget); const autoJsActive = await waitForPayPalPaymentMethodActive(2500); if (autoJsActive) { log('已按 auto.js 方式确认 PayPal 选项生效'); } else { log('auto.js 方式点击 PayPal 后未观察到标准选中标记,继续按 hosted checkout 宽松模式执行。', 'warn'); } return { paymentSelected: autoJsActive, relaxed: !autoJsActive, }; } const target = await waitForPayPalPaymentMethodTarget(10000); if (!target) { if (options.relaxedActivation) { log('未找到 PayPal 选项按钮,当前为宽松模式,继续执行...', 'warn'); return { paymentSelected: false, relaxed: true }; } throw new Error('未找到 PayPal 付款方式,无法切换。'); } const clickTargets = getPayPalActivationTargets(target); for (let attempt = 0; attempt < 2; attempt += 1) { for (const candidate of clickTargets) { dispatchRobustActivation(candidate); await sleep(220); if (hasSelectedPayPalControl() || hasBillingAddressFields()) { break; } } await sleep(450); } const active = await waitForPayPalPaymentMethodActive(3500); if (!active) { log('点击 PayPal 后未观察到标准选中标记,继续按 hosted checkout 宽松模式执行。', 'warn'); } else { log('已确认 PayPal 选项生效'); } return { paymentSelected: active, relaxed: !active, }; } async function fillStripeBillingAddress(payload = {}) { const addr = normalizeCheckoutAddress(payload.address || {}); if (payload.autoJsDirectSelectors) { log('参考 auto.js 直接填写账单字段...'); fillBillingAddressDirectSelectors(addr); await sleep(800); hideAutocompleteDropdowns(); await ensureTermsCheckbox(); const latest = getStructuredAddressFields(); return { countryText: readCountryText(), selectedAutocompleteAddress: false, structuredAddress: { address1: latest.address1?.value || '', city: latest.city?.value || '', region: latest.region?.value || getSelectText(latest.regionSelect) || '', postalCode: latest.postalCode?.value || '', }, }; } log('正在填写账单地址...'); const selectedAutocompleteAddress = await fillBillingAddressLine1FromGoogle(addr); if (!selectedAutocompleteAddress) { log('未能选择 Google 地址下拉项,改用手动填写地址兜底。', 'warn'); const fields = getStructuredAddressFields(); const address1Input = fields.address1 || findAddressSearchInput(); if (address1Input) { fillInput(address1Input, addr.street); } else { fillBySelector('#billingAddressLine1', addr.street); } } await sleep(900); const fields = getStructuredAddressFields(); fillMissingControl(fields.city || document.querySelector('#billingLocality'), addr.city); fillMissingControl(fields.postalCode || document.querySelector('#billingPostalCode'), addr.zip.substring(0, 5)); fillRegionControlByText(fields.regionSelect || fields.region || document.querySelector('#billingAdministrativeArea'), addr.state); await sleep(800); hideAutocompleteDropdowns(); await ensureTermsCheckbox(); const latest = getStructuredAddressFields(); return { countryText: readCountryText(), selectedAutocompleteAddress, structuredAddress: { address1: latest.address1?.value || '', city: latest.city?.value || '', region: latest.region?.value || getSelectText(latest.regionSelect) || '', postalCode: latest.postalCode?.value || '', }, }; } async function clickStripeCheckoutSubmit(payload = {}) { await ensureTermsCheckbox(); await sleep(Math.max(0, Math.floor(Number(payload.beforeClickDelayMs) || 0))); await clickSubmitButton(); return { clicked: true }; } function normalizeCheckoutAddress(addr = {}) { return { street: normalizeText(addr.street || addr.address1 || '123 Main St'), city: normalizeText(addr.city || 'New York'), state: normalizeText(addr.state || addr.region || 'New York'), zip: normalizeText(addr.zip || addr.postalCode || '10001').substring(0, 5), query: normalizeText(addr.query || addr.autocompleteQuery || ''), }; } function getStructuredAddressFields() { const address1 = getVisibleElementById('billingAddressLine1') || findInputByFieldText([ /address\s*(?:line)?\s*1|address[_-]?line[_-]?1|address\[(?:address_)?line1\]|line\s*1|street|street[_-]?address/i, /地址\s*1|街道|详细地址|住所/i, ], { exclude: (input) => isNonAddressSearchInput(input), }) || findAddressSearchInput(); const city = getVisibleElementById('billingLocality') || findInputByFieldText([ /city|town|suburb|locality|address[_-]?level[_-]?2|address\[city\]/i, /城市|市区|区市町村|市区町村|市町村/i, ]); const postalCode = getVisibleElementById('billingPostalCode') || findInputByFieldText([ /postal|zip|postcode|postal[_-]?code|zip[_-]?code|address\[postal_code\]/i, /邮编|邮政|郵便番号/i, ]); const regionSelect = getVisibleElementById('billingAdministrativeArea'); const region = regionSelect || findInputByFieldText([ /state|province|region|county|prefecture|administrative|administrative[_-]?area|address[_-]?level[_-]?1|address\[state\]/i, /省|州|地区|辖区|都道府县|都道府県/i, ]); return { address1, city, postalCode, region, regionSelect: regionSelect && regionSelect.tagName === 'SELECT' ? regionSelect : null, }; } function getVisibleElementById(id) { const el = document.getElementById(id); return el && isVisibleNode(el) ? el : null; } function hasBillingAddressFields(fields = getStructuredAddressFields()) { if (fields?.address1 || fields?.city || fields?.postalCode) { return true; } return getVisibleTextInputs().some((input) => { const text = getFieldText(input); return /address|street|billing|line\s*1|地址|街道|账单/i.test(text) && !/card\s*number|card|expiry|expiration|security|cvc|cvv|name|email|e-mail|phone|tel|country|region|postal|zip|city|state|province|银行卡|卡号|有效期|安全码|姓名|邮箱|电话|国家|地区|邮编|城市|省|州/i.test(text); }) || Boolean(findAddressSearchInput()); } function readCountryText() { const country = document.getElementById('billingCountry') || findInputByFieldText([/country|region/i, /国家|地区/i]) || Array.from(document.querySelectorAll('select')).find((select) => ( isVisibleNode(select) && /country|region|国家|地区/i.test(getFieldText(select)) )); if (!country) return ''; if (country.tagName === 'SELECT') { return getSelectText(country) || country.value || ''; } return country.value || country.textContent || ''; } function getSelectText(select) { if (!select || select.tagName !== 'SELECT') return ''; return normalizeText(select.selectedOptions?.[0]?.textContent || select.value || ''); } function findInputByFieldText(patterns = [], options = {}) { const excluded = options.exclude || (() => false); return getVisibleTextInputs().find((input) => { if (excluded(input)) return false; return patterns.some((pattern) => pattern.test(getFieldText(input))); }) || null; } function getVisibleControls(selector) { return Array.from(document.querySelectorAll(selector)).filter((el) => isVisibleNode(el)); } function getVisibleTextInputs() { return getVisibleControls('input, textarea') .filter((el) => { const type = String(el.getAttribute('type') || el.type || '').trim().toLowerCase(); return !['hidden', 'checkbox', 'radio', 'submit', 'button', 'file'].includes(type); }); } function getVisibleFormControls() { return Array.from(document.querySelectorAll('input, textarea, select')) .filter((el) => { const type = String(el.getAttribute('type') || el.type || '').toLowerCase(); return type !== 'hidden' && isVisibleNode(el); }); } function getFieldText(el) { if (!el) return ''; const id = el.id ? String(el.id) : ''; const labelText = id ? Array.from(document.querySelectorAll(`label[for="${cssEscape(id)}"]`)).map((label) => label.textContent || '').join(' ') : ''; const wrappingLabel = el.closest?.('label')?.textContent || ''; const container = el.closest?.('[data-testid], [class], div, section, fieldset'); return normalizeText([ id, el.name, el.getAttribute?.('autocomplete'), el.getAttribute?.('aria-label'), el.getAttribute?.('placeholder'), labelText, wrappingLabel, container && !isDocumentLevelContainer(container) ? container.textContent || '' : '', ].filter(Boolean).join(' ')); } function getDirectFieldHintText(el) { if (!el) return ''; const id = el.id ? String(el.id) : ''; const labelText = id ? Array.from(document.querySelectorAll(`label[for="${cssEscape(id)}"]`)).map((label) => label.textContent || '').join(' ') : ''; const wrappingLabel = el.closest?.('label')?.textContent || ''; return normalizeText([ id, el.name, el.getAttribute?.('autocomplete'), el.getAttribute?.('aria-label'), el.getAttribute?.('placeholder'), labelText, wrappingLabel, ].filter(Boolean).join(' ')); } function isNonAddressSearchInput(input) { const directText = getDirectFieldHintText(input); const type = String(input?.getAttribute?.('type') || input?.type || '').trim().toLowerCase(); return /name|email|e-mail|phone|tel|password|coupon|promo|country|region|postal|zip|city|state|province|card|card\s*number|expiry|expiration|security|cvc|cvv|cc-|全名|姓名|邮箱|电话|密码|国家|地区|邮编|城市|省|州|银行卡|卡号|有效期|安全码/i.test(directText) || ['email', 'tel', 'password'].includes(type); } function isLikelyAddressSearchInput(input) { const text = getFieldText(input); if (isNonAddressSearchInput(input)) { return false; } if (/name|email|e-mail|phone|tel|password|coupon|promo|country|region|postal|zip|city|state|province|card|card\s*number|expiry|expiration|security|cvc|cvv|cc-|全名|姓名|邮箱|电话|密码|国家|地区|邮编|城市|省|州|银行卡|卡号|有效期|安全码/i.test(text)) { return false; } return /address|street|billing|search|line\s*1|地址|街道|账单/i.test(text); } function findAddressSearchInput() { const direct = findInputByFieldText([ /address|street|billing|search|line\s*1/i, /地址|街道|账单/i, ], { exclude: (input) => isNonAddressSearchInput(input) || /city|state|province|postal|zip|country|region|城市|省|州|邮编|国家|地区/i.test(getFieldText(input)), }); if (direct) return direct; return getVisibleTextInputs().filter(isLikelyAddressSearchInput)[0] || null; } function isDocumentLevelContainer(el) { return !el || el === document.documentElement || el === document.body || ['HTML', 'BODY', 'MAIN'].includes(el.tagName); } function cssEscape(value) { if (window.CSS?.escape) return window.CSS.escape(value); return String(value || '').replace(/["\\]/g, '\\$&'); } function findPayPalPaymentMethodTarget() { const directSelectors = [ '[data-testid="paypal-accordion-item-button"]', '[data-testid*="paypal" i]', '.paypal-accordion-item button', 'button[aria-label*="PayPal" i]', '[aria-label*="PayPal" i]', '[title*="PayPal" i]', '[role="radio"][aria-label*="PayPal" i]', 'input[type="radio"][value*="paypal" i]', 'button[value*="paypal" i]', ]; for (const selector of directSelectors) { const target = Array.from(document.querySelectorAll(selector)).find((el) => isVisibleNode(el)); if (target) return target; } const directClickable = findClickableByText([/paypal/i]); if (directClickable) return directClickable; const radios = getVisibleControls('input[type="radio"], [role="radio"]'); const matchedRadio = radios.find((el) => /paypal/i.test(getCombinedSearchText(el))); if (matchedRadio) return matchedRadio; for (const candidate of getPayPalSearchCandidates()) { const interactive = findInteractiveAncestor(candidate); if (interactive && /paypal/i.test(getCombinedSearchText(interactive))) { return interactive; } const card = findPaymentCardAncestor(candidate, /paypal/i); if (card) { return card; } } return null; } function getPayPalCandidateSummaries() { return getPayPalSearchCandidates() .slice(0, 8) .map((el) => ({ tag: el.tagName, id: el.id || '', role: el.getAttribute?.('role') || '', text: normalizeText(getCombinedSearchText(el)).slice(0, 120), visible: isVisibleNode(el), checked: el.checked === true || el.getAttribute?.('aria-checked') === 'true', })); } function findClickableByText(patterns = []) { const candidates = getVisibleControls('button, a, [role="button"], [role="radio"], [role="tab"], input[type="button"], input[type="submit"], input[type="radio"], label, [tabindex]'); return candidates.find((el) => { const text = getCombinedSearchText(el); return patterns.some((pattern) => pattern.test(text)); }) || null; } function getPayPalActivationTargets(target) { const candidates = []; const push = (el) => { if (!el || !isVisibleNode(el) || isDocumentLevelContainer(el)) return; if (!candidates.includes(el)) candidates.push(el); }; push(target); push(target?.querySelector?.('input[type="radio"], [role="radio"], button, [role="button"]')); push(target?.closest?.('input[type="radio"], [role="radio"], button, [role="button"], label, [tabindex]')); push(findInteractiveAncestor(target)); push(findPaymentCardAncestor(target, /paypal/i)); let current = target; for (let depth = 0; current && depth < 7; depth += 1, current = current.parentElement) { if (isDocumentLevelContainer(current)) break; if (/paypal/i.test(getCombinedSearchText(current))) { push(current.querySelector?.('input[type="radio"], [role="radio"], button, [role="button"]')); if (isPaymentCardSized(current)) push(current); if (current.matches?.('button, [role="button"], [role="radio"], label, [tabindex]')) push(current); } } for (const candidate of getPayPalSearchCandidates().slice(0, 8)) { push(candidate.closest?.('button, [role="button"], [role="radio"], label, [tabindex]')); push(findInteractiveAncestor(candidate)); push(findPaymentCardAncestor(candidate, /paypal/i)); } return candidates.slice(0, 10); } function getPayPalSearchCandidates() { const selector = [ 'button', 'a', 'label', '[role="button"]', '[role="radio"]', '[role="tab"]', 'input[type="radio"]', '[tabindex]', '[data-testid]', '[aria-label]', '[title]', 'img', 'svg', 'span', 'div', ].join(', '); return getVisibleControls(selector) .filter((el) => /paypal/i.test(getCombinedSearchText(el))) .sort((left, right) => { const leftRect = left.getBoundingClientRect(); const rightRect = right.getBoundingClientRect(); return (leftRect.width * leftRect.height) - (rightRect.width * rightRect.height); }); } function findInteractiveAncestor(el) { let current = el; for (let depth = 0; current && depth < 8; depth += 1, current = current.parentElement) { if (!isVisibleNode(current) || isDocumentLevelContainer(current)) continue; if (current.matches?.('button, a, label, [role="button"], [role="radio"], [role="tab"], input[type="radio"], [tabindex]')) { return current; } } return null; } function isPaymentCardSized(el) { if (!isVisibleNode(el) || isDocumentLevelContainer(el)) return false; const rect = el.getBoundingClientRect(); const maxWidth = Math.max(320, Math.min(window.innerWidth * 0.95, 900)); const maxHeight = Math.max(140, Math.min(window.innerHeight * 0.45, 340)); return rect.width >= 64 && rect.height >= 28 && rect.width <= maxWidth && rect.height <= maxHeight; } function findPaymentCardAncestor(el, pattern) { let current = el; for (let depth = 0; current && depth < 8; depth += 1, current = current.parentElement) { if (!isVisibleNode(current)) continue; if (isDocumentLevelContainer(current)) break; const text = getCombinedSearchText(current); if (pattern.test(text) && isPaymentCardSized(current)) { return current; } } return null; } function getCombinedSearchText(el) { if (!el) return ''; return [ el.textContent, el.getAttribute?.('aria-label'), el.getAttribute?.('data-testid'), el.id, el.name, el.value, el.className && typeof el.className === 'string' ? el.className : '', ].filter(Boolean).join(' '); } async function waitForPayPalPaymentMethodTarget(timeoutMs = 10000) { const startedAt = Date.now(); while (Date.now() - startedAt < timeoutMs) { throwIfStopped(); const target = findPayPalPaymentMethodTarget(); if (target) return target; await sleep(250); } return null; } async function waitForPayPalPaymentMethodActive(timeoutMs = 3500) { const startedAt = Date.now(); while (Date.now() - startedAt < timeoutMs) { throwIfStopped(); if (hasSelectedPayPalControl()) return true; await sleep(250); } return false; } function hasSelectedPayPalControl() { const target = findPayPalPaymentMethodTarget(); let current = target; for (let depth = 0; current && depth < 6; depth += 1, current = current.parentElement) { if (isDocumentLevelContainer(current)) break; if (/paypal/i.test(getCombinedSearchText(current)) && hasPaymentMethodSelectionMarker(current)) { return true; } const radio = current.querySelector?.('input[type="radio"], [role="radio"]'); if ( radio && /paypal/i.test(getCombinedSearchText(current) || getCombinedSearchText(radio)) && hasPaymentMethodSelectionMarker(radio) ) { return true; } } return false; } function hasPaymentMethodSelectionMarker(el) { if (!el) return false; const className = typeof el.className === 'string' ? el.className : el.getAttribute?.('class') || ''; return el.checked === true || el.getAttribute?.('aria-checked') === 'true' || el.getAttribute?.('aria-selected') === 'true' || el.getAttribute?.('data-state') === 'checked' || el.getAttribute?.('data-selected') === 'true' || /\b(selected|checked|active)\b/i.test(className); } async function ensureTermsCheckbox() { const cb = document.getElementById('termsOfServiceConsentCheckbox') || Array.from(document.querySelectorAll('input[type="checkbox"]')).find((input) => /terms|service|agree|consent/i.test(getFieldText(input))); if (cb && !cb.checked) { dispatchPointerMouseClick(cb); log('已勾选服务条款'); await sleep(300); } } function findSubmitButton() { const direct = document.querySelector('button[data-testid="submit-button"]') || document.querySelector('button[data-testid="hosted-payment-submit-button"]') || document.querySelector('button[data-atomic-wait-intent="Submit_Email"]') || document.querySelector('button.SubmitButton--complete'); if (direct && isVisibleNode(direct)) return direct; const patterns = [/^下一页$/, /^下一步$/, /^next$/i, /subscribe|pay|continue|agree/i, /訂閱|处理中|同意|付款|继续/]; return Array.from(document.querySelectorAll('button, [role="button"], input[type="submit"]')).find((el) => { if (!isVisibleNode(el) || el.disabled) return false; const text = normalizeText(el.textContent || el.value || el.getAttribute?.('aria-label') || ''); return patterns.some((pattern) => pattern.test(text)); }) || null; } function dispatchRobustActivation(el) { dispatchPointerMouseClick(el); if (typeof el.focus === 'function') { el.focus({ preventScroll: true }); } [' ', 'Enter'].forEach((key) => { try { el.dispatchEvent(new KeyboardEvent('keydown', { key, code: key === ' ' ? 'Space' : 'Enter', bubbles: true, cancelable: true, })); el.dispatchEvent(new KeyboardEvent('keyup', { key, code: key === ' ' ? 'Space' : 'Enter', bubbles: true, cancelable: true, })); } catch { // Some synthetic keyboard events can be rejected on hardened checkout nodes. } }); } function dispatchPointerMouseClick(el) { if (!el) throw new Error('无法点击空元素。'); el.scrollIntoView?.({ block: 'center', inline: 'nearest' }); const rect = el.getBoundingClientRect(); const clientX = Math.max(0, Math.floor(rect.left + rect.width / 2)); const clientY = Math.max(0, Math.floor(rect.top + rect.height / 2)); ['pointerdown', 'mouseover', 'mousedown', 'pointerup', 'mouseup', 'click'].forEach((type) => { const EventCtor = type.startsWith('pointer') && typeof PointerEvent === 'function' ? PointerEvent : MouseEvent; el.dispatchEvent(new EventCtor(type, { bubbles: true, cancelable: true, view: window, button: 0, buttons: type === 'pointerup' || type === 'mouseup' || type === 'click' ? 0 : 1, clientX, clientY, pointerId: 1, pointerType: 'mouse', isPrimary: true, })); }); if (typeof el.click === 'function') { el.click(); } } function fillBySelector(selector, value) { const el = document.querySelector(selector); if (el) { fillInput(el, value); } else { log(`未找到元素: ${selector}`, 'warn'); } } function fillMissingBySelector(selector, value) { const el = document.querySelector(selector); if (!el) { log(`未找到元素: ${selector}`, 'warn'); return; } if (String(el.value || '').trim()) { return; } fillInput(el, value); } function fillMissingControl(el, value) { if (!el) return false; if (String(el.value || '').trim()) { return false; } fillInput(el, value); return true; } function fillSelectByText(selector, text) { const el = document.querySelector(selector); if (!el) { log(`未找到 select: ${selector}`, 'warn'); return; } for (let i = 0; i < el.options.length; i++) { const opt = el.options[i]; if (opt.text.toLowerCase().includes(text.toLowerCase()) || opt.value.toLowerCase().includes(text.toLowerCase())) { fillSelect(el, opt.value); return; } } log(`未在 select 中找到匹配项: ${text}`, 'warn'); } function fillRegionControlByText(el, text) { if (!el) return false; if (el.tagName === 'SELECT') { for (let i = 0; i < el.options.length; i++) { const opt = el.options[i]; if (opt.text.toLowerCase().includes(text.toLowerCase()) || opt.value.toLowerCase().includes(text.toLowerCase())) { fillSelect(el, opt.value); return true; } } log(`未在地区 select 中找到匹配项: ${text}`, 'warn'); return false; } return fillMissingControl(el, text); } function fillBillingAddressDirectSelectors(addr = {}) { fillBySelector('#billingAddressLine1', addr.street); fillBySelector('#billingLocality', addr.city); fillBySelector('#billingPostalCode', addr.zip.substring(0, 5)); fillSelectByText('#billingAdministrativeArea', addr.state); } async function fillBillingAddressLine1FromGoogle(addr = {}) { const input = getStructuredAddressFields().address1 || findAddressSearchInput(); if (!input) { log('未找到地址栏 1 输入框,无法触发 Google 地址下拉。', 'warn'); return false; } const query = buildGoogleAddressQuery(addr); log(`正在地址栏 1 输入完整地址以触发 Google 下拉: ${query}`); await typeAddressQueryForAutocomplete(input, query); const item = await waitForGoogleAddressSuggestion(query, 6500); if (item) { const itemText = getSuggestionText(item) || '首个地址建议'; log(`正在选择 Google 地址建议: ${itemText}`); clickAutocompleteSuggestion(item); await waitForAddressAutofill(input, query, 2200); dispatchInputBlur(input); return true; } const externalFrameSelected = await selectAddressSuggestionInExternalFrame(query); if (externalFrameSelected) { await waitForAddressAutofill(input, query, 2200); dispatchInputBlur(input); return true; } log('未检测到可点击的 Google 地址建议,尝试使用键盘选择首个建议...', 'warn'); const keyboardSelected = await chooseAutocompleteWithKeyboard(input, query); if (keyboardSelected) { dispatchInputBlur(input); return true; } return false; } function findAutoJsPayPalTarget() { return document.querySelector('[data-testid="paypal-accordion-item-button"]') || document.querySelector('.paypal-accordion-item button'); } async function clickPayPalLikeAutoJs(target) { if (!target) return; target.scrollIntoView?.({ block: 'center', inline: 'nearest' }); if (typeof target.click === 'function') { target.click(); } await sleep(600); } async function selectGoogleAddressSuggestionOnly(payload = {}) { const query = normalizeText(payload.query || ''); const item = await waitForGoogleAddressSuggestion(query, 5500); if (!item) { return { error: '未找到 Google 地址建议项' }; } const itemText = getSuggestionText(item) || '首个地址建议'; log(`正在独立 autocomplete iframe 中选择 Google 地址建议: ${itemText}`); clickAutocompleteSuggestion(item); await sleep(900); return { ok: true, selectedAddressText: itemText, }; } async function selectAddressSuggestionInExternalFrame(query) { if (!chrome?.runtime?.sendMessage) { return false; } try { const response = await chrome.runtime.sendMessage({ type: 'CHECKOUT_STRIPE_SELECT_AUTOCOMPLETE_FRAME', source: 'checkout-stripe', payload: { query }, }); if (response?.ok) { log(`已在独立 Google 地址 iframe 中选择建议: ${response.selectedAddressText || '首个地址建议'}`); return true; } if (response?.error) { log(`独立 Google 地址 iframe 未完成选择: ${response.error}`, 'warn'); } } catch (error) { log(`尝试选择独立 Google 地址 iframe 失败: ${error?.message || error}`, 'warn'); } return false; } function buildGoogleAddressQuery(addr = {}) { const explicitQuery = normalizeText(addr.query || addr.autocompleteQuery || ''); if (explicitQuery) return explicitQuery; const street = normalizeText(addr.street || addr.address1 || '123 Main St'); const city = normalizeText(addr.city || 'New York'); const state = normalizeText(addr.state || addr.region || 'New York'); const zip = normalizeText(addr.zip || addr.postalCode || '10001').substring(0, 5); return [street, city, state, zip].filter(Boolean).join(', '); } async function typeAddressQueryForAutocomplete(input, query) { input.scrollIntoView?.({ block: 'center', inline: 'nearest' }); input.focus(); await sleep(150); setNativeInputValue(input, ''); input.dispatchEvent(new Event('input', { bubbles: true })); input.dispatchEvent(new Event('change', { bubbles: true })); await sleep(120); for (const char of String(query || '')) { throwIfStopped(); input.dispatchEvent(new KeyboardEvent('keydown', { key: char, bubbles: true, cancelable: true, })); setNativeInputValue(input, `${input.value || ''}${char}`); dispatchAutocompleteInputEvent(input, char); input.dispatchEvent(new KeyboardEvent('keyup', { key: char, bubbles: true, cancelable: true, })); await sleep(18); } input.dispatchEvent(new Event('change', { bubbles: true })); } function setNativeInputValue(input, value) { const descriptor = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value'); if (descriptor?.set) { descriptor.set.call(input, value); } else { input.value = value; } } function dispatchAutocompleteInputEvent(input, data) { try { input.dispatchEvent(new InputEvent('input', { bubbles: true, cancelable: true, data, inputType: 'insertText', })); } catch { input.dispatchEvent(new Event('input', { bubbles: true, cancelable: true })); } } function dispatchInputBlur(input) { input.dispatchEvent(new Event('change', { bubbles: true })); input.dispatchEvent(new Event('blur', { bubbles: true })); } async function waitForGoogleAddressSuggestion(query, timeoutMs = 5000) { const start = Date.now(); while (Date.now() - start < timeoutMs) { throwIfStopped(); const items = getVisibleAutocompleteSuggestions(query); if (items.length) { const matching = findBestAddressSuggestion(items, query); return matching || items[0]; } await sleep(250); } return null; } function getVisibleAutocompleteSuggestions(query = '') { const selectors = [ { selector: '.pac-container .pac-item', generic: false }, { selector: '#billing-address-autocomplete-results [role="option"]', generic: false }, { selector: '.AddressAutocomplete-results [role="option"]', generic: false }, { selector: '[class*="AddressAutocomplete"] [role="option"]', generic: false }, { selector: '[data-testid*="address" i] [role="option"]', generic: false }, { selector: '[role="listbox"] [role="option"]', generic: false }, { selector: '[role="option"]', generic: false }, { selector: '[role="listbox"] li', generic: true }, { selector: '.autocomplete-dropdown [role="option"]', generic: false }, { selector: '.autocomplete-dropdown li', generic: true }, { selector: 'li', generic: true }, ]; const seen = new Set(); const items = []; for (const config of selectors) { document.querySelectorAll(config.selector).forEach((item) => { if (seen.has(item) || !isVisibleNode(item)) return; if (!isLikelyAddressSuggestion(item, query, config.generic)) return; seen.add(item); items.push(item); }); } return items; } function findBestAddressSuggestion(items, query) { let best = null; let bestScore = 0; for (const item of items) { const score = scoreAddressSuggestion(item, query); if (score > bestScore) { best = item; bestScore = score; } } return best || null; } function getSuggestionText(item) { return normalizeText(item?.textContent || item?.getAttribute?.('aria-label') || ''); } function normalizeText(value = '') { return String(value || '').replace(/\s+/g, ' ').trim(); } function getAddressQueryTokens(query = '') { return normalizeText(query) .toLowerCase() .split(/[^a-z0-9]+/i) .map((part) => part.trim()) .filter((part) => part.length >= 3); } function scoreAddressSuggestion(item, query = '') { const text = getSuggestionText(item).toLowerCase(); if (!text) return 0; const tokens = getAddressQueryTokens(query); let score = 0; tokens.forEach((token) => { if (text.includes(token)) score += 1; }); if (/\d/.test(text)) score += 2; if (/street|st\.?|avenue|ave\.?|road|rd\.?|drive|dr\.?|boulevard|blvd\.?|lane|ln\.?|way|court|ct\.?/i.test(text)) { score += 2; } return score; } function isLikelyAddressSuggestion(item, query = '', generic = false) { const text = getSuggestionText(item); if (!text || text.length < 3) return false; if (!generic) return true; const lowered = text.toLowerCase(); if (/terms|privacy|subscribe|paypal|card|payment|email|phone|下一步|提交|付款|订阅/i.test(lowered)) { return false; } return scoreAddressSuggestion(item, query) > 0; } function isVisibleNode(node) { const rect = node.getBoundingClientRect(); const style = window.getComputedStyle(node); return rect.width > 0 && rect.height > 0 && style.visibility !== 'hidden' && style.display !== 'none'; } function clickAutocompleteSuggestion(item) { item.scrollIntoView?.({ block: 'nearest', inline: 'nearest' }); ['pointerdown', 'mouseover', 'mousedown', 'pointerup', 'mouseup', 'click'].forEach((type) => { const EventCtor = type.startsWith('pointer') && typeof PointerEvent === 'function' ? PointerEvent : MouseEvent; item.dispatchEvent(new EventCtor(type, { bubbles: true, cancelable: true, view: window, })); }); if (typeof item.click === 'function') { item.click(); } } async function chooseAutocompleteWithKeyboard(input, query = '') { input.focus(); input.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', code: 'ArrowDown', bubbles: true, cancelable: true })); input.dispatchEvent(new KeyboardEvent('keyup', { key: 'ArrowDown', code: 'ArrowDown', bubbles: true, cancelable: true })); await sleep(250); input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', code: 'Enter', bubbles: true, cancelable: true })); input.dispatchEvent(new KeyboardEvent('keyup', { key: 'Enter', code: 'Enter', bubbles: true, cancelable: true })); return waitForAddressAutofill(input, query, 2200); } async function waitForAddressAutofill(input, query = '', timeoutMs = 1800) { const startedAt = Date.now(); const originalQuery = normalizeText(query); while (Date.now() - startedAt < timeoutMs) { throwIfStopped(); const line1 = normalizeText(input?.value || ''); const fields = getStructuredAddressFields(); const city = normalizeText(fields.city?.value || document.querySelector('#billingLocality')?.value || ''); const zip = normalizeText(fields.postalCode?.value || document.querySelector('#billingPostalCode')?.value || ''); if (city || zip || (line1 && line1 !== originalQuery)) { return true; } await sleep(200); } return false; } function hideAutocompleteDropdowns() { log('正在隐藏剩余 Google 地址补全框...'); document.querySelectorAll([ '.pac-container', '.pac-item', 'div[role="listbox"]', '.AddressAutocomplete-results', '[class*="AddressAutocomplete"]', '#billing-address-autocomplete-results', ].join(', ')).forEach((el) => { el.style.setProperty('display', 'none', 'important'); el.style.setProperty('visibility', 'hidden', 'important'); el.style.setProperty('height', '0', 'important'); el.style.setProperty('overflow', 'hidden', 'important'); }); } async function clickSubmitButton(retries = 0) { throwIfStopped(); const btn = findSubmitButton(); if (btn) { const rect = btn.getBoundingClientRect(); if (btn.disabled || rect.height === 0) { log('提交按钮被禁用或不可见,等待中...'); if (retries < 12) { await sleep(800); return clickSubmitButton(retries + 1); } throw new Error('提交按钮一直不可用,已超时'); } log(`正在点击提交按钮: ${btn.textContent.trim()}`); dispatchPointerMouseClick(btn); } else { if (retries < 12) { log(`未找到提交按钮,重试中... (${retries + 1})`); await sleep(800); return clickSubmitButton(retries + 1); } throw new Error('在 Stripe 页面上未找到提交按钮'); } } document.documentElement.setAttribute('data-multipage-checkout-stripe-ready', ''); })();