Merge remote-tracking branch 'origin/dev' into dev
This commit is contained in:
+21
-2
@@ -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,
|
||||
|
||||
@@ -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: '步骤 6:ChatGPT 页面仍在加载,等待 Plus Checkout 脚本就绪...',
|
||||
});
|
||||
|
||||
@@ -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(`步骤 7:meiguodizhi 地址接口不可用,回退到本地地址种子:${error?.message || String(error || '')}`, 'warn');
|
||||
}
|
||||
|
||||
return fallbackSeed;
|
||||
if (hasCompleteAddressFallback(localSeed)) {
|
||||
return localSeed;
|
||||
}
|
||||
throw new Error(`步骤 7:${requestedCountry} 的 meiguodizhi 地址不可用,且没有本地兜底地址。`);
|
||||
}
|
||||
|
||||
async function getAlivePlusCheckoutTabId(tabId) {
|
||||
|
||||
+166
-13
@@ -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
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -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
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user