Merge remote-tracking branch 'origin/dev' into dev

This commit is contained in:
QLHazycoder
2026-04-26 08:38:16 +00:00
13 changed files with 837 additions and 171 deletions
+21 -2
View File
@@ -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;
}
@@ -7051,6 +7067,9 @@ const plusCheckoutCreateExecutor = self.MultiPageBackgroundPlusCheckoutCreate?.c
chrome,
completeStepFromBackground,
ensureContentScriptReadyOnTabUntilStopped,
getTabId,
isTabAlive,
registerTab,
reuseOrCreateTab,
sendTabMessageUntilStopped,
setState,
+52 -5
View File
@@ -2,8 +2,21 @@
root.MultiPageBackgroundPlusCheckoutCreate = factory();
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundPlusCheckoutCreateModule() {
const PLUS_CHECKOUT_SOURCE = 'plus-checkout';
const SIGNUP_PAGE_SOURCE = 'signup-page';
const PLUS_CHECKOUT_ENTRY_URL = 'https://chatgpt.com/';
const PLUS_CHECKOUT_INJECT_FILES = ['content/utils.js', 'content/plus-checkout.js'];
const PLUS_CHECKOUT_ONLY_INJECT_FILES = ['content/plus-checkout.js'];
function isReusableChatGptTabUrl(url = '') {
try {
const parsed = new URL(String(url || ''));
const hostname = parsed.hostname.toLowerCase();
return ['chatgpt.com', 'www.chatgpt.com', 'chat.openai.com'].includes(hostname)
&& !/^\/checkout(?:\/|$)/i.test(parsed.pathname || '');
} catch {
return false;
}
}
function createPlusCheckoutCreateExecutor(deps = {}) {
const {
@@ -11,6 +24,9 @@
chrome,
completeStepFromBackground,
ensureContentScriptReadyOnTabUntilStopped,
getTabId,
isTabAlive,
registerTab,
reuseOrCreateTab,
sendTabMessageUntilStopped,
setState,
@@ -18,18 +34,49 @@
waitForTabCompleteUntilStopped,
} = deps;
async function executePlusCheckoutCreate() {
await addLog('步骤 6:正在打开 ChatGPT 会话页,准备创建 Plus Checkout...', 'info');
async function getReusableSignupChatGptTabId() {
if (typeof getTabId !== 'function' || typeof isTabAlive !== 'function') {
return null;
}
const tabId = await getTabId(SIGNUP_PAGE_SOURCE);
if (!tabId || !(await isTabAlive(SIGNUP_PAGE_SOURCE))) {
return null;
}
const tab = await chrome.tabs.get(tabId).catch(() => null);
return tab && isReusableChatGptTabUrl(tab.url) ? tabId : null;
}
async function resolveChatGptTabForCheckoutCreate() {
const existingSignupTabId = await getReusableSignupChatGptTabId();
if (existingSignupTabId) {
await chrome.tabs.update(existingSignupTabId, { active: true });
if (typeof registerTab === 'function') {
await registerTab(PLUS_CHECKOUT_SOURCE, existingSignupTabId);
}
await addLog('步骤 6:检测到第 5 步已打开 ChatGPT 页面,直接接管当前标签页创建 Plus Checkout。', 'info');
return {
tabId: existingSignupTabId,
injectFiles: PLUS_CHECKOUT_ONLY_INJECT_FILES,
};
}
const tabId = await reuseOrCreateTab(PLUS_CHECKOUT_SOURCE, PLUS_CHECKOUT_ENTRY_URL, {
inject: PLUS_CHECKOUT_INJECT_FILES,
injectSource: PLUS_CHECKOUT_SOURCE,
reloadIfSameUrl: false,
});
return {
tabId,
injectFiles: PLUS_CHECKOUT_INJECT_FILES,
};
}
async function executePlusCheckoutCreate() {
await addLog('步骤 6:正在打开 ChatGPT 会话页,准备创建 Plus Checkout...', 'info');
const { tabId, injectFiles } = await resolveChatGptTabForCheckoutCreate();
await waitForTabCompleteUntilStopped(tabId);
await sleepWithStop(1000);
await ensureContentScriptReadyOnTabUntilStopped(PLUS_CHECKOUT_SOURCE, tabId, {
inject: PLUS_CHECKOUT_INJECT_FILES,
inject: injectFiles,
injectSource: PLUS_CHECKOUT_SOURCE,
logMessage: '步骤 6ChatGPT 页面仍在加载,等待 Plus Checkout 脚本就绪...',
});
+88 -17
View File
@@ -6,11 +6,29 @@
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', '越南'] },
};
function createPlusCheckoutBillingExecutor(deps = {}) {
@@ -38,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(
@@ -48,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;
@@ -75,8 +114,12 @@
if (typeof fetchImpl !== 'function') {
return null;
}
const path = MEIGUODIZHI_PATH_BY_COUNTRY[countryCode] || MEIGUODIZHI_PATH_BY_COUNTRY.DE;
const city = normalizeText(fallbackSeed?.fallback?.city || fallbackSeed?.query || '');
const countryConfig = MEIGUODIZHI_COUNTRY_CONFIG[countryCode];
if (!countryConfig?.path) {
return null;
}
const path = countryConfig.path;
const city = normalizeText(fallbackSeed?.fallback?.city || fallbackSeed?.query || countryConfig.city);
const response = await fetchImpl(MEIGUODIZHI_ADDRESS_ENDPOINT, {
method: 'POST',
headers: {
@@ -98,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 地址推荐。`,
@@ -122,7 +190,10 @@
await addLog(`步骤 7meiguodizhi 地址接口不可用,回退到本地地址种子:${error?.message || String(error || '')}`, 'warn');
}
return fallbackSeed;
if (hasCompleteAddressFallback(localSeed)) {
return localSeed;
}
throw new Error(`步骤 7${requestedCountry} 的 meiguodizhi 地址不可用,且没有本地兜底地址。`);
}
async function getAlivePlusCheckoutTabId(tabId) {
+166 -13
View File
@@ -1,6 +1,8 @@
// content/plus-checkout.js — ChatGPT Plus checkout helper.
(function attachPlusCheckoutContentScript() {
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 = {
@@ -502,6 +504,10 @@ function readCountryText() {
const option = countrySelect.selectedOptions?.[0];
return option?.textContent || countrySelect.value || '';
}
const countryDropdown = findCountryDropdown();
if (countryDropdown) {
return getCountryDropdownValue(countryDropdown);
}
return '';
}
@@ -663,14 +669,16 @@ function getRegionCandidates(value) {
tas: 'Tasmania',
vic: 'Victoria',
wa: 'Western Australia',
tokyo: '東京都',
osaka: '大阪府',
};
const compact = raw.toLowerCase().replace(/[^a-z0-9]/g, '');
const compact = raw.toLowerCase().replace(/[^a-z0-9\u4e00-\u9fff]/g, '');
const candidates = [raw];
if (aliases[compact]) {
candidates.push(aliases[compact]);
}
for (const [abbr, name] of Object.entries(aliases)) {
const compactName = name.toLowerCase().replace(/[^a-z0-9]/g, '');
const compactName = name.toLowerCase().replace(/[^a-z0-9\u4e00-\u9fff]/g, '');
if (compact === compactName) {
candidates.push(abbr.toUpperCase());
}
@@ -678,13 +686,90 @@ function getRegionCandidates(value) {
return Array.from(new Set(candidates.filter(Boolean)));
}
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]) => {
if (String(code).toLowerCase() === compact) return true;
return names.some((name) => {
const normalizedName = normalizeText(name).toLowerCase();
const compactName = normalizedName.replace(/[^a-z0-9\u4e00-\u9fff]/g, '');
return compact === compactName || normalizedName === raw.toLowerCase();
});
});
return Array.from(new Set([raw, ...direct, ...(matched ? matched[1] : [])].filter(Boolean)));
}
function matchesCountryOption(text, desiredValue) {
const normalizedText = normalizeText(text).toLowerCase();
const compactText = normalizedText.replace(/[^a-z0-9\u4e00-\u9fff]/g, '');
if (!compactText) return false;
return getCountryCandidates(desiredValue).some((candidate) => {
const normalizedCandidate = normalizeText(candidate).toLowerCase();
const compactCandidate = normalizedCandidate.replace(/[^a-z0-9\u4e00-\u9fff]/g, '');
if (!compactCandidate) return false;
return normalizedText === normalizedCandidate
|| compactText === compactCandidate
|| (compactCandidate.length > 3 && compactText.includes(compactCandidate));
});
}
function findCountryDropdown() {
const controls = getVisibleControls('select, button, [role="button"], [role="combobox"], [aria-haspopup="listbox"]');
return controls.find((control) => {
if (!isEnabledControl(control) || isDocumentLevelContainer(control)) return false;
const text = getFieldText(control);
return /country/i.test(text) || /\u56fd\u5bb6|\u56fd\u5bb6\u6216\u5730\u533a/.test(text);
}) || null;
}
function getCountryDropdownValue(control) {
if (!control) return '';
if (String(control.tagName || '').toUpperCase() === 'SELECT') {
const selected = control.selectedOptions?.[0];
return normalizeText(selected?.textContent || control.value || '');
}
return normalizeText(
control.getAttribute?.('aria-valuetext')
|| control.getAttribute?.('aria-label')
|| control.getAttribute?.('data-value')
|| control.textContent
|| ''
);
}
function matchesRegionOption(text, desiredValue) {
const normalizedText = normalizeText(text).toLowerCase();
const compactText = normalizedText.replace(/[^a-z0-9]/g, '');
const compactText = normalizedText.replace(/[^a-z0-9\u4e00-\u9fff]/g, '');
if (!compactText) return false;
return getRegionCandidates(desiredValue).some((candidate) => {
const normalizedCandidate = normalizeText(candidate).toLowerCase();
const compactCandidate = normalizedCandidate.replace(/[^a-z0-9]/g, '');
const compactCandidate = normalizedCandidate.replace(/[^a-z0-9\u4e00-\u9fff]/g, '');
if (!compactCandidate) return false;
return normalizedText === normalizedCandidate
|| compactText === compactCandidate
@@ -700,7 +785,7 @@ function findRegionDropdown() {
if (/country/i.test(text) || /\u56fd\u5bb6|\u5730\u533a/.test(text)) return false;
return /state|province|county/i.test(text)
|| /(?:^|\s)region(?:\s|$)/i.test(text)
|| /\u5dde|\u7701/.test(text);
|| /\u5dde|\u7701|\u8f96\u533a|\u90fd\u9053\u5e9c\u53bf/.test(text);
}) || null;
}
@@ -785,6 +870,53 @@ async function selectRegionDropdown(regionDropdown, value) {
return true;
}
async function selectCountryDropdown(countryDropdown, value) {
if (!countryDropdown || !value) return false;
if (matchesCountryOption(getCountryDropdownValue(countryDropdown), value)) {
return false;
}
if (String(countryDropdown.tagName || '').toUpperCase() === 'SELECT') {
const option = Array.from(countryDropdown.options || []).find((item) => (
matchesCountryOption(item.textContent || '', value)
|| matchesCountryOption(item.value || '', value)
));
if (!option) {
throw new Error(`Plus Checkout: country dropdown option "${value}" was not found.`);
}
countryDropdown.value = option.value;
option.selected = true;
countryDropdown.dispatchEvent(new Event('input', { bubbles: true }));
countryDropdown.dispatchEvent(new Event('change', { bubbles: true }));
await sleep(500);
return true;
}
simulateClick(countryDropdown);
await sleep(250);
const startedAt = Date.now();
let option = null;
while (Date.now() - startedAt < 2500) {
throwIfStopped();
option = getVisibleRegionOptions().find((item) => (
matchesCountryOption(getActionText(item) || item.textContent || '', value)
));
if (option) break;
await sleep(100);
}
if (!option) {
const visibleOptions = getVisibleRegionOptions()
.map((item) => normalizeText(getActionText(item) || item.textContent || ''))
.filter(Boolean)
.slice(0, 12)
.join(' | ');
throw new Error(`Plus Checkout: country dropdown option "${value}" was not found. Visible options: ${visibleOptions || 'none'}.`);
}
simulateClick(option);
await sleep(700);
return true;
}
function getStructuredAddressFields() {
const address1 = findInputByFieldText([
/address\s*(?:line)?\s*1|street/i,
@@ -809,15 +941,31 @@ function getStructuredAddressFields() {
return { address1, address2, city, region, postalCode };
}
function fillIfEmpty(input, value) {
function fillIfEmpty(input, value, options = {}) {
if (!input || !value) return false;
if (String(input.value || '').trim()) return false;
if (!options.overwrite && String(input.value || '').trim()) return false;
if (options.overwrite && String(input.value || '').trim() === String(value || '').trim()) return false;
fillInput(input, value);
return true;
}
async function ensureStructuredAddress(seed) {
function isDropdownStructuredAddressForm(fields = getStructuredAddressFields()) {
return Boolean(
findCountryDropdown()
&& findRegionDropdown()
&& fields.address1
&& fields.city
&& fields.postalCode
);
}
async function ensureStructuredAddress(seed, options = {}) {
const fallback = seed?.fallback || {};
const overwrite = Boolean(options.overwrite);
const countryDropdown = findCountryDropdown();
if (countryDropdown && seed?.countryCode) {
await selectCountryDropdown(countryDropdown, seed.countryCode);
}
const fields = await waitUntil(() => {
const currentFields = getStructuredAddressFields();
if (currentFields.address1 || currentFields.city || currentFields.postalCode) {
@@ -829,10 +977,10 @@ async function ensureStructuredAddress(seed) {
intervalMs: 250,
});
fillIfEmpty(fields.address1, fallback.address1);
fillIfEmpty(fields.city, fallback.city);
fillIfEmpty(fields.address1, fallback.address1, { overwrite });
fillIfEmpty(fields.city, fallback.city, { overwrite });
await selectRegionDropdown(findRegionDropdown(), fallback.region);
fillIfEmpty(fields.postalCode, fallback.postalCode);
fillIfEmpty(fields.postalCode, fallback.postalCode, { overwrite });
await sleep(500);
const latest = getStructuredAddressFields();
@@ -894,10 +1042,14 @@ async function fillPlusBillingAddress(payload = {}) {
},
};
let selected = { selectedText: '' };
if (!seed.skipAutocomplete) {
const fields = getStructuredAddressFields();
const useDirectStructuredBranch = Boolean(seed.skipAutocomplete || isDropdownStructuredAddressForm(fields));
if (!useDirectStructuredBranch) {
selected = await selectAddressSuggestion(seed);
}
const structuredAddress = await ensureStructuredAddress(seed);
const structuredAddress = await ensureStructuredAddress(seed, {
overwrite: useDirectStructuredBranch,
});
return {
countryText,
@@ -970,3 +1122,4 @@ function inspectPlusCheckoutState() {
},
};
}
})();
+11 -6
View File
@@ -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();
}
+23
View File
@@ -5,6 +5,7 @@
AU: ['au', 'aus', 'australia', '澳大利亚'],
DE: ['de', 'deu', 'germany', 'deutschland', '德国'],
FR: ['fr', 'fra', 'france', '法国'],
JP: ['jp', 'jpn', 'japan', '日本', '日本国'],
US: ['us', 'usa', 'united states', 'united states of america', 'america', '美国'],
};
@@ -75,6 +76,28 @@
},
},
],
JP: [
{
query: 'Tokyo Marunouchi',
suggestionIndex: 1,
fallback: {
address1: 'Marunouchi 1-1',
city: 'Chiyoda-ku',
region: 'Tokyo',
postalCode: '100-0005',
},
},
{
query: 'Osaka Umeda',
suggestionIndex: 1,
fallback: {
address1: 'Umeda 3-1',
city: 'Kita-ku',
region: 'Osaka',
postalCode: '530-0001',
},
},
],
US: [
{
query: 'New York NY',
+84 -7
View File
@@ -1,12 +1,13 @@
# Codex 注册扩展相关项目、更新、邮箱切换、PayPal 与 Clash Verge 配置教程
# Codex 注册扩展相关项目、更新、邮箱、PayPal 与 Clash Verge 配置教程
本教程用于说明相关项目地址、扩展更新方式、`Cloudflare Temp Email``QQ 邮箱` 的使用方法、`PayPal` 注册绑卡流程,以及 [Clash Verge](https://github.com/clash-verge-rev/clash-verge-rev) 的 `🔁 非港轮询` 配置方法。
本教程用于说明相关项目地址、扩展更新方式、`Cloudflare Temp Email``iCloud 隐私邮箱``QQ 邮箱` 的使用方法、`PayPal` 注册绑卡流程,以及 [Clash Verge](https://github.com/clash-verge-rev/clash-verge-rev) 的 `🔁 非港轮询` 配置方法。
## 适用场景
- 需要拉取并部署 `cpa``sub2api` 项目
- 已经安装本扩展,想更新到最新版本
- 需要把 `Cloudflare Temp Email` 用作 `邮箱生成``邮箱服务`
- 需要使用 `iCloud+``隐藏邮件地址` 作为隐私邮箱
- 需要临时切换 `QQ 邮箱` 地址继续使用
- 需要注册并使用 `PayPal` 个人账户
- 需要在 `Clash Verge` 中启用 `🔁 非港轮询`
@@ -17,6 +18,8 @@
- 已安装好的扩展文件夹
- 可以打开浏览器的 `扩展程序管理` 页面
- 已准备好 `Cloudflare Temp Email` 后端地址;如需随机子域,域名解析也已配置完成
- 一个已开通 `iCloud+` 的 Apple ID
- 一个用于接收转发邮件的邮箱
- 一个可正常登录的 `QQ 邮箱`
- 一个可正常接收短信的手机号
- 一张可在线支付的借记卡或信用卡
@@ -100,7 +103,67 @@
8. 查看搭建参考
如果你还没有部署后端,可参考 [LINUX DO 教程](https://linux.do/t/topic/316819)。
### 第四部分:`QQ 邮箱`切换邮箱使用教程
### 第四部分:`iCloud 隐私邮箱` 使用方法
1. 准备 Apple ID
如果条件允许,建议使用非日常主力 Apple ID。
这样即使触发风控,也不会影响你平时常用的 Apple ID。
2.`iPhone``iPad` 上开通并启用 `iCloud 邮件`
打开 `设置` App。
点击顶部你的姓名,进入 `Apple ID` 设置。
点击 `iCloud`
找到 `邮件`,打开右侧开关。
如果是首次开通,系统会提示你创建电子邮件地址。
输入你想要的邮箱前缀,例如 `yourname`
系统会自动检查名称是否可用。
点击 `下一步` 后,即可创建 `yourname@icloud.com` 邮箱。
3.`Mac` 上启用 `iCloud 邮件`
点击屏幕左上角的苹果菜单。
打开 `系统设置`
点击你的姓名,进入 `Apple ID` 设置。
点击 `iCloud`
在应用列表中找到 `iCloud 邮件`,确认开关已经打开。
如果是首次开通,系统会引导你创建新的 `@icloud.com` 邮箱地址,按屏幕提示输入前缀即可完成。
4. 进入 `隐藏邮件地址`
`iCloud` 页面往下滑,找到 `iCloud+` 服务。
点击 `隐藏邮件地址`
这里就是后续要使用的隐私邮箱功能。
5. 先手动创建一批隐私邮箱
建议先手动生成一个隐私邮箱确认流程可用。
如果插件端直接生成,可能会遇到冷却时间或风控。
更稳妥的做法是先手动创建一批,例如 `20` 个左右,让插件后续自动读取。
同时确认 `转发至` 已经设置为你要接收邮件的邮箱。
完成后,手机端或平板端的准备工作就结束了。
6. 配置插件端的邮箱服务
在插件里把 `邮箱服务` 选择为你用于接收转发邮件的邮箱。
然后登录该邮箱,确保能正常收信。
7. 配置插件端的邮箱生成
`邮件生成` 选择为 `iCloud 隐私邮箱`
往下滑到对应配置区域。
先在网页中登录你的 `iCloud`
回到插件的 `隐私邮箱配置` 中点击刷新,等待插件拉取你已经创建好的隐私邮箱。
8. 选择邮箱使用方式
建议选择 `复用未使用的`
如果你希望使用后释放数量,可以勾选 `使用后删除`
如果不删除,隐私邮箱可以保留为长期邮箱使用。
目前观察到,如果一直不删除,手动创建到 `20` 多个后可能会提示数量已满,后续需要继续观察具体原因。
9. 控制创建频率
建议每天最多新建 `3` 个左右。
测试中一天创建过多会触发 `iCloud` 风控提示。
10. 查看 Apple 官方说明
`iCloud 邮件` 主地址创建说明:[Create a primary email address for iCloud Mail](https://support.apple.com/is-is/guide/icloud/mmdd8d1c5c/icloud)
`隐藏邮件地址` 创建与转发说明:[Create and edit Hide My Email addresses on iCloud.com](https://support.apple.com/lv-lv/guide/icloud/mm1a876f7aed/icloud)
### 第五部分:`QQ 邮箱`切换邮箱使用教程
1. 登录 `QQ 邮箱`
先登录你当前正在使用的 `QQ 邮箱` 账号。
@@ -119,7 +182,7 @@
这两个邮箱地址使用完成后,可以直接删除。
删除后再次创建新的英文邮箱和 `Foxmail` 邮箱,即可重复注册使用。
### 第部分:`PayPal` 注册与绑卡使用教程
### 第部分:`PayPal` 注册与绑卡使用教程
1. 打开注册页面
打开 [https://www.paypal.com/signin](https://www.paypal.com/signin)。
@@ -171,14 +234,14 @@
常见情况是上传身份证件。
`PayPal` 官方帮助中心说明,通常会在 `2 个工作日` 内审核,但某些情况可能更久。
### 第部分:0元试用 ChatGPT Plus 教程
### 第部分:0元试用 ChatGPT Plus 教程
本部分说明如何在已登录 ChatGPT 的状态下,通过脚本快速生成 Plus 支付链接,然后选择 PayPal 支付完成0元试用订阅。
#### 准备工作
1. 已有一个登录状态的 ChatGPT 账户。
2. 一个可用的 PayPal 账户(参考第部分进行注册和绑卡)。
2. 一个可用的 PayPal 账户(参考第部分进行注册和绑卡)。
3. 能够接收生成的账单地址的真实地址或虚拟地址。
4. Chrome 浏览器(推荐使用地址补全功能)。
@@ -326,7 +389,7 @@
- **PayPal 登录后页面无法继续跳转**
稍等片刻让页面加载完毕。如果长时间未响应,检查浏览器是否有弹窗被隐藏,或尝试刷新页面。
### 第部分:配置 [Clash Verge](https://github.com/clash-verge-rev/clash-verge-rev) 的 `🔁 非港轮询`
### 第部分:配置 [Clash Verge](https://github.com/clash-verge-rev/clash-verge-rev) 的 `🔁 非港轮询`
#### 第一步:添加扩展脚本
@@ -448,6 +511,17 @@ function main(config, profileName) {
可以。你在 `账号管理` 中创建英文邮箱和 `Foxmail` 邮箱,使用后直接删除,再重复创建即可继续使用。
### `iCloud 隐私邮箱` 插件刷新后没有邮箱怎么办?
先确认你已经在网页中登录 `iCloud`,并且已经在 `隐藏邮件地址` 里手动创建过隐私邮箱。
然后回到插件的 `隐私邮箱配置` 里重新刷新,等待插件拉取已创建的邮箱。
### `iCloud 隐私邮箱` 要不要勾选 `使用后删除`?
如果你想让邮箱数量及时释放,可以勾选 `使用后删除`
如果你想把某些地址当作长期邮箱保留,可以不删除。
需要注意的是,如果一直不删除,隐私邮箱数量可能会很快达到上限。
### `PayPal` 绑卡时报错怎么办?
先去 `钱包` 页面再看一眼,有时虽然页面提示报错,但实际上已经绑定成功。
@@ -469,6 +543,9 @@ function main(config, profileName) {
- 不论使用哪种方式更新扩展,更新完成后都必须在浏览器的 `扩展程序管理` 页面重新加载一次该扩展。
- 如果同时把 `Cloudflare Temp Email` 用作 `邮箱生成``邮箱服务`,请同时检查 `Admin Auth``Custom Auth``Temp 域名``邮件接收` 是否都已配置。
- 开启 `随机子域` 前,请先确认后端已经配置 `RANDOM_SUBDOMAIN_DOMAINS`,并且 Cloudflare DNS 已完成 `MX *` 设置。
- 使用 `iCloud 隐私邮箱` 前,建议先手动创建一批地址,再让插件读取并复用。
- `iCloud 隐私邮箱` 不建议短时间大量新建,测试中一天创建过多会触发 `iCloud` 风控提示,建议每天最多新建 `3` 个左右。
- 如果条件允许,建议使用非日常主力 Apple ID 配置 `iCloud 隐私邮箱`,避免影响平时常用账号。
- 如需部署 `cpa`,请先确认部署环境可以访问 `OpenAI`,否则可能出现第十步认证成功但没有认证文件的问题。
- `QQ 邮箱`切换邮箱时,建议在使用完成后及时删除,再重新创建,避免混淆当前正在使用的邮箱地址。
- 中国大陆居民注册 `PayPal` 个人账户时,姓名请按身份证上的中文姓名填写,不要使用拼音。
+33 -29
View File
@@ -1,17 +1,28 @@
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 = [
(function attachMailProviderUtils(root, factory) {
const api = factory();
if (typeof module !== 'undefined' && module.exports) {
module.exports = api;
}
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 normalizeMailProvider(value = '') {
function normalizeMailProvider(value = '') {
const normalized = String(value || '').trim().toLowerCase();
switch (normalized) {
case HOTMAIL_PROVIDER:
@@ -24,26 +35,26 @@ function normalizeMailProvider(value = '') {
default:
return '163';
}
}
}
function normalizeIcloudTargetMailboxType(value = '') {
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 = '') {
function normalizeIcloudForwardMailProvider(value = '') {
const normalized = String(value || '').trim().toLowerCase();
return ICLOUD_FORWARD_MAIL_PROVIDER_OPTIONS.some((option) => option.value === normalized)
? normalized
: 'qq';
}
}
function getIcloudForwardMailProviderOptions() {
function getIcloudForwardMailProviderOptions() {
return ICLOUD_FORWARD_MAIL_PROVIDER_OPTIONS.map((option) => ({ ...option }));
}
}
function getIcloudForwardMailConfig(provider = 'qq') {
function getIcloudForwardMailConfig(provider = 'qq') {
const normalizedProvider = normalizeIcloudForwardMailProvider(provider);
if (normalizedProvider === GMAIL_PROVIDER) {
return {
@@ -56,9 +67,9 @@ function getIcloudForwardMailConfig(provider = 'qq') {
}
return getMailProviderConfig({ mailProvider: normalizedProvider });
}
}
function getMailProviderConfig(state = {}, options = {}) {
function getMailProviderConfig(state = {}, options = {}) {
const provider = normalizeMailProvider(state.mailProvider);
const normalizeInbucketOrigin = options.normalizeInbucketOrigin || (() => '');
@@ -105,9 +116,9 @@ function getMailProviderConfig(state = {}, options = {}) {
};
}
return { source: 'qq-mail', url: 'https://wx.mail.qq.com/', label: 'QQ 邮箱' };
}
}
const api = {
return {
GMAIL_PROVIDER,
HOTMAIL_PROVIDER,
getIcloudForwardMailConfig,
@@ -116,12 +127,5 @@ const api = {
normalizeIcloudForwardMailProvider,
normalizeIcloudTargetMailboxType,
normalizeMailProvider,
};
if (typeof module !== 'undefined' && module.exports) {
module.exports = api;
}
if (typeof self !== 'undefined') {
self.MailProviderUtils = api;
}
};
});
+5
View File
@@ -9,6 +9,7 @@ test('address sources normalize supported countries and return local seeds', ()
assert.equal(api.normalizeCountryCode('Deutschland'), 'DE');
assert.equal(api.normalizeCountryCode('澳大利亚'), 'AU');
assert.equal(api.normalizeCountryCode('日本'), 'JP');
assert.equal(api.normalizeCountryCode('unknown'), '');
const deSeed = api.getAddressSeedForCountry('DE');
@@ -20,4 +21,8 @@ test('address sources normalize supported countries and return local seeds', ()
const fallbackSeed = api.getAddressSeedForCountry('unknown', { fallbackCountry: 'AU' });
assert.equal(fallbackSeed.countryCode, 'AU');
assert.equal(fallbackSeed.fallback.region, 'New South Wales');
const jpSeed = api.getAddressSeedForCountry('日本');
assert.equal(jpSeed.countryCode, 'JP');
assert.equal(jpSeed.fallback.region, 'Tokyo');
});
+13
View File
@@ -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');
});
+120
View File
@@ -1,9 +1,43 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const vm = require('node:vm');
const source = fs.readFileSync('content/plus-checkout.js', 'utf8');
test('plus checkout content script can be injected repeatedly on the same page', () => {
const attrs = new Map();
const context = {
console: { log() {}, warn() {}, error() {}, info() {} },
location: { href: 'https://chatgpt.com/' },
window: {},
document: {
documentElement: {
getAttribute(name) {
return attrs.get(name) || null;
},
setAttribute(name, value) {
attrs.set(name, String(value));
},
},
},
chrome: {
runtime: {
onMessage: {
addListener() {},
},
},
},
};
context.window = context;
vm.createContext(context);
vm.runInContext(source, context);
vm.runInContext(source, context);
assert.equal(context.__MULTIPAGE_PLUS_CHECKOUT_READY__, true);
});
function extractFunction(name) {
const plainStart = source.indexOf(`function ${name}(`);
const asyncStart = source.indexOf(`async function ${name}(`);
@@ -253,3 +287,89 @@ return { selectRegionDropdown };
assert.deepEqual(clicks, [stateDropdown, options[1]]);
});
test('country and region helpers recognize the dropdown-style localized address form', () => {
const bundle = [
extractFunction('isVisibleElement'),
extractFunction('normalizeText'),
extractFunction('getActionText'),
extractFunction('getFieldText'),
extractFunction('getVisibleControls'),
extractFunction('isEnabledControl'),
extractFunction('isDocumentLevelContainer'),
extractFunction('getCountryCandidates'),
extractFunction('matchesCountryOption'),
extractFunction('findCountryDropdown'),
extractFunction('getRegionCandidates'),
extractFunction('matchesRegionOption'),
extractFunction('findRegionDropdown'),
].join('\n');
const countryDropdown = createElement({
tagName: 'DIV',
text: '国家或地区 日本',
attrs: {
role: 'combobox',
'aria-haspopup': 'listbox',
},
});
const regionDropdown = createElement({
tagName: 'DIV',
text: '辖区 选择',
attrs: {
role: 'combobox',
'aria-haspopup': 'listbox',
},
});
const elements = [countryDropdown, regionDropdown];
const documentMock = {
documentElement: {},
body: {},
querySelectorAll: (selector) => {
if (String(selector || '').includes('label[for=')) return [];
if (String(selector || '').includes('combobox') || String(selector || '').includes('button') || String(selector || '').includes('select')) {
return elements;
}
return [];
},
};
const windowMock = {
getComputedStyle: () => ({ display: 'block', visibility: 'visible' }),
};
const cssMock = {
escape: (value) => String(value),
};
const api = new Function('window', 'document', 'CSS', `
${bundle}
return { findCountryDropdown, findRegionDropdown, matchesCountryOption, matchesRegionOption };
`)(windowMock, documentMock, cssMock);
assert.equal(api.findCountryDropdown(), countryDropdown);
assert.equal(api.findRegionDropdown(), regionDropdown);
assert.equal(api.matchesCountryOption('日本', 'JP'), true);
assert.equal(api.matchesCountryOption('德国', 'DE'), true);
assert.equal(api.matchesRegionOption('東京都', 'Tokyo'), true);
});
test('fillIfEmpty can overwrite invalid structured address values in the dropdown branch', () => {
const bundle = [
extractFunction('fillIfEmpty'),
].join('\n');
const input = { value: '77022' };
const writes = [];
const api = new Function('input', 'writes', `
function fillInput(el, value) {
writes.push(value);
el.value = value;
}
${bundle}
return { fillIfEmpty };
`)(input, writes);
assert.equal(api.fillIfEmpty(input, '100-0005'), false);
assert.equal(input.value, '77022');
assert.equal(api.fillIfEmpty(input, '100-0005', { overwrite: true }), true);
assert.equal(input.value, '100-0005');
assert.deepEqual(writes, ['100-0005']);
});
@@ -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: [
+80 -1
View File
@@ -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]);
@@ -54,3 +62,74 @@ test('Plus checkout create does not wait 20 seconds after opening checkout page'
assert.equal(events.some((event) => event.type === 'sleep' && event.ms === 20000), false);
assert.equal(events.some((event) => event.type === 'log' && /固定等待 20 秒后继续下一步/.test(event.message)), false);
});
test('Plus checkout create reuses the ChatGPT tab from signup step 5', async () => {
const events = [];
const executor = api.createPlusCheckoutCreateExecutor({
addLog: async (message, level = 'info') => {
events.push({ type: 'log', message, level });
},
chrome: {
tabs: {
get: async (tabId) => {
events.push({ type: 'tab-get', tabId });
return { id: tabId, url: 'https://chatgpt.com/' };
},
update: async (tabId, payload) => {
events.push({ type: 'tab-update', tabId, payload });
},
},
},
completeStepFromBackground: async (step, payload) => {
events.push({ type: 'complete', step, payload });
},
ensureContentScriptReadyOnTabUntilStopped: async (source, tabId, options) => {
events.push({ type: 'ready', source, tabId, options });
},
getTabId: async (source) => {
events.push({ type: 'get-tab-id', source });
return source === 'signup-page' ? 55 : null;
},
isTabAlive: async (source) => {
events.push({ type: 'alive', source });
return source === 'signup-page';
},
registerTab: async (source, tabId) => {
events.push({ type: 'register', source, tabId });
},
reuseOrCreateTab: async () => {
events.push({ type: 'reuse-tab' });
return 42;
},
sendTabMessageUntilStopped: async () => ({
checkoutUrl: 'https://checkout.stripe.com/c/pay/session',
country: 'US',
currency: 'USD',
}),
setState: async (payload) => {
events.push({ type: 'set-state', payload });
},
sleepWithStop: async (ms) => {
events.push({ type: 'sleep', ms });
},
waitForTabCompleteUntilStopped: async (tabId) => {
events.push({ type: 'tab-complete', tabId });
},
});
await executor.executePlusCheckoutCreate();
assert.equal(events.some((event) => event.type === 'reuse-tab'), false);
assert.deepEqual(
events.find((event) => event.type === 'register'),
{ type: 'register', source: 'plus-checkout', tabId: 55 }
);
assert.deepEqual(
events.find((event) => event.type === 'ready').options.inject,
['content/plus-checkout.js']
);
assert.equal(
events.some((event) => event.type === 'log' && /直接接管当前标签页/.test(event.message)),
true
);
});