feat: 添加 meiguodizhi 地址接口支持,优化账单地址获取逻辑,跳过 Google 地址推荐
This commit is contained in:
@@ -6924,6 +6924,7 @@ const plusCheckoutBillingExecutor = self.MultiPageBackgroundPlusCheckoutBilling?
|
||||
chrome,
|
||||
completeStepFromBackground,
|
||||
ensureContentScriptReadyOnTabUntilStopped,
|
||||
fetch: typeof fetch === 'function' ? fetch.bind(globalThis) : null,
|
||||
generateRandomName,
|
||||
getAddressSeedForCountry: self.MultiPageAddressSources?.getAddressSeedForCountry,
|
||||
getTabId,
|
||||
|
||||
@@ -5,6 +5,13 @@
|
||||
const PLUS_CHECKOUT_INJECT_FILES = ['content/utils.js', 'content/plus-checkout.js'];
|
||||
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: '/',
|
||||
};
|
||||
|
||||
function createPlusCheckoutBillingExecutor(deps = {}) {
|
||||
const {
|
||||
@@ -12,6 +19,7 @@
|
||||
chrome,
|
||||
completeStepFromBackground,
|
||||
ensureContentScriptReadyOnTabUntilStopped,
|
||||
fetch: fetchImpl = null,
|
||||
generateRandomName,
|
||||
getAddressSeedForCountry,
|
||||
getTabId,
|
||||
@@ -26,6 +34,97 @@
|
||||
return PLUS_CHECKOUT_URL_PATTERN.test(String(url || ''));
|
||||
}
|
||||
|
||||
function normalizeText(value = '') {
|
||||
return String(value || '').replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
|
||||
function hasCompleteAddressFallback(seed) {
|
||||
const fallback = seed?.fallback || {};
|
||||
return Boolean(
|
||||
normalizeText(fallback.address1)
|
||||
&& normalizeText(fallback.city)
|
||||
&& normalizeText(fallback.postalCode)
|
||||
);
|
||||
}
|
||||
|
||||
function buildDirectAddressSeed(countryCode, apiAddress, fallbackSeed) {
|
||||
const address1 = normalizeText(apiAddress?.Address);
|
||||
const city = normalizeText(apiAddress?.City);
|
||||
const region = normalizeText(apiAddress?.State || apiAddress?.State_Full);
|
||||
const postalCode = normalizeText(apiAddress?.Zip_Code);
|
||||
if (!address1 || !city || !postalCode) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
...(fallbackSeed || {}),
|
||||
countryCode,
|
||||
query: [address1, city].filter(Boolean).join(', '),
|
||||
source: 'meiguodizhi',
|
||||
skipAutocomplete: true,
|
||||
fallback: {
|
||||
...(fallbackSeed?.fallback || {}),
|
||||
address1,
|
||||
city,
|
||||
region,
|
||||
postalCode,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function fetchMeiguodizhiAddressSeed(countryCode, fallbackSeed) {
|
||||
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 response = await fetchImpl(MEIGUODIZHI_ADDRESS_ENDPOINT, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
city,
|
||||
path,
|
||||
method: 'refresh',
|
||||
}),
|
||||
});
|
||||
if (!response?.ok) {
|
||||
throw new Error(`HTTP ${response?.status || 0}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
if (data?.status !== 'ok') {
|
||||
throw new Error(data?.message || data?.status || 'unknown response');
|
||||
}
|
||||
return buildDirectAddressSeed(countryCode, data.address || {}, fallbackSeed);
|
||||
}
|
||||
|
||||
async function resolveBillingAddressSeed(state = {}, countryOverride = '') {
|
||||
const requestedCountry = normalizeText(countryOverride || state.plusCheckoutCountry || 'DE');
|
||||
const fallbackSeed = getAddressSeedForCountry(requestedCountry, {
|
||||
fallbackCountry: 'DE',
|
||||
});
|
||||
if (!fallbackSeed) {
|
||||
throw new Error('步骤 7:未找到可用的本地账单地址种子。');
|
||||
}
|
||||
|
||||
const countryCode = fallbackSeed.countryCode || 'DE';
|
||||
try {
|
||||
const remoteSeed = await fetchMeiguodizhiAddressSeed(countryCode, fallbackSeed);
|
||||
if (hasCompleteAddressFallback(remoteSeed)) {
|
||||
await addLog(
|
||||
`步骤 7:已从 meiguodizhi 接口获取账单地址(${remoteSeed.fallback.city} / ${remoteSeed.fallback.postalCode}),将跳过 Google 地址推荐。`,
|
||||
'info'
|
||||
);
|
||||
return remoteSeed;
|
||||
}
|
||||
await addLog('步骤 7:meiguodizhi 接口返回的地址字段不完整,回退到本地地址种子。', 'warn');
|
||||
} catch (error) {
|
||||
await addLog(`步骤 7:meiguodizhi 地址接口不可用,回退到本地地址种子:${error?.message || String(error || '')}`, 'warn');
|
||||
}
|
||||
|
||||
return fallbackSeed;
|
||||
}
|
||||
|
||||
async function getAlivePlusCheckoutTabId(tabId) {
|
||||
if (!Number.isInteger(tabId) || tabId <= 0) {
|
||||
return null;
|
||||
@@ -241,6 +340,7 @@
|
||||
return {
|
||||
frameId: picked.frame.frameId,
|
||||
frameUrl: picked.frame.url || '',
|
||||
countryText: picked.result?.countryText || '',
|
||||
ready: picked.frame.ready !== false,
|
||||
inspections,
|
||||
};
|
||||
@@ -312,12 +412,6 @@
|
||||
|
||||
const randomName = generateRandomName();
|
||||
const fullName = [randomName.firstName, randomName.lastName].filter(Boolean).join(' ');
|
||||
const addressSeed = getAddressSeedForCountry(state.plusCheckoutCountry || 'DE', {
|
||||
fallbackCountry: 'DE',
|
||||
});
|
||||
if (!addressSeed) {
|
||||
throw new Error('步骤 7:未找到可用的本地账单地址种子。');
|
||||
}
|
||||
|
||||
await addLog('步骤 7:正在切换 PayPal 付款方式...', 'info');
|
||||
const paymentResult = await sendFrameMessage(tabId, paymentFrame.frameId, {
|
||||
@@ -337,10 +431,15 @@
|
||||
await addLog(`步骤 7:账单地址位于 checkout iframe(frameId=${billingFrame.frameId}),将改为在该 frame 内填写。`, 'info');
|
||||
}
|
||||
|
||||
const addressSeed = await resolveBillingAddressSeed(state, billingFrame.countryText);
|
||||
if (!addressSeed) {
|
||||
throw new Error('步骤 7:未找到可用的本地账单地址种子。');
|
||||
}
|
||||
|
||||
await addLog(`步骤 7:正在填写账单地址(${addressSeed.countryCode} / ${addressSeed.query})...`, 'info');
|
||||
const autocompleteFrame = await resolveOptionalFrameByUrl(tabId, isAutocompleteFrameUrl);
|
||||
let result = null;
|
||||
if (autocompleteFrame?.frame && autocompleteFrame.frame.frameId !== billingFrame.frameId) {
|
||||
if (!addressSeed.skipAutocomplete && autocompleteFrame?.frame && autocompleteFrame.frame.frameId !== billingFrame.frameId) {
|
||||
if (!autocompleteFrame.ready) {
|
||||
throw new Error('步骤 7:发现 Google 地址推荐 iframe,但无法注入账单脚本。请提供该 iframe 的控制台结构。');
|
||||
}
|
||||
|
||||
+141
-4
@@ -651,6 +651,140 @@ async function fillAddressQuery(seed = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
function getRegionCandidates(value) {
|
||||
const raw = normalizeText(value);
|
||||
if (!raw) return [];
|
||||
const aliases = {
|
||||
act: 'Australian Capital Territory',
|
||||
nsw: 'New South Wales',
|
||||
nt: 'Northern Territory',
|
||||
qld: 'Queensland',
|
||||
sa: 'South Australia',
|
||||
tas: 'Tasmania',
|
||||
vic: 'Victoria',
|
||||
wa: 'Western Australia',
|
||||
};
|
||||
const compact = raw.toLowerCase().replace(/[^a-z0-9]/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, '');
|
||||
if (compact === compactName) {
|
||||
candidates.push(abbr.toUpperCase());
|
||||
}
|
||||
}
|
||||
return Array.from(new Set(candidates.filter(Boolean)));
|
||||
}
|
||||
|
||||
function matchesRegionOption(text, desiredValue) {
|
||||
const normalizedText = normalizeText(text).toLowerCase();
|
||||
const compactText = normalizedText.replace(/[^a-z0-9]/g, '');
|
||||
if (!compactText) return false;
|
||||
return getRegionCandidates(desiredValue).some((candidate) => {
|
||||
const normalizedCandidate = normalizeText(candidate).toLowerCase();
|
||||
const compactCandidate = normalizedCandidate.replace(/[^a-z0-9]/g, '');
|
||||
if (!compactCandidate) return false;
|
||||
return normalizedText === normalizedCandidate
|
||||
|| compactText === compactCandidate
|
||||
|| (compactCandidate.length > 3 && compactText.includes(compactCandidate));
|
||||
});
|
||||
}
|
||||
|
||||
function findRegionDropdown() {
|
||||
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);
|
||||
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);
|
||||
}) || null;
|
||||
}
|
||||
|
||||
function getRegionDropdownValue(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 getVisibleRegionOptions() {
|
||||
const selectors = [
|
||||
'[role="listbox"] [role="option"]',
|
||||
'[role="option"]',
|
||||
'li',
|
||||
];
|
||||
const seen = new Set();
|
||||
const options = [];
|
||||
for (const selector of selectors) {
|
||||
for (const option of Array.from(document.querySelectorAll(selector))) {
|
||||
if (!isVisibleElement(option)) continue;
|
||||
const text = normalizeText(getActionText(option) || option.textContent || '');
|
||||
if (!text || seen.has(text)) continue;
|
||||
seen.add(text);
|
||||
options.push(option);
|
||||
}
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
async function selectRegionDropdown(regionDropdown, value) {
|
||||
if (!regionDropdown || !value) return false;
|
||||
if (matchesRegionOption(getRegionDropdownValue(regionDropdown), value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (String(regionDropdown.tagName || '').toUpperCase() === 'SELECT') {
|
||||
const option = Array.from(regionDropdown.options || []).find((item) => (
|
||||
matchesRegionOption(item.textContent || '', value)
|
||||
|| matchesRegionOption(item.value || '', value)
|
||||
));
|
||||
if (!option) {
|
||||
throw new Error(`Plus Checkout: state dropdown option "${value}" was not found.`);
|
||||
}
|
||||
regionDropdown.value = option.value;
|
||||
option.selected = true;
|
||||
regionDropdown.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
regionDropdown.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
return true;
|
||||
}
|
||||
|
||||
simulateClick(regionDropdown);
|
||||
await sleep(250);
|
||||
const startedAt = Date.now();
|
||||
let option = null;
|
||||
while (Date.now() - startedAt < 2500) {
|
||||
throwIfStopped();
|
||||
option = getVisibleRegionOptions().find((item) => (
|
||||
matchesRegionOption(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: state dropdown option "${value}" was not found. Visible options: ${visibleOptions || 'none'}.`);
|
||||
}
|
||||
simulateClick(option);
|
||||
await sleep(500);
|
||||
return true;
|
||||
}
|
||||
|
||||
function getStructuredAddressFields() {
|
||||
const address1 = findInputByFieldText([
|
||||
/address\s*(?:line)?\s*1|street/i,
|
||||
@@ -697,7 +831,7 @@ async function ensureStructuredAddress(seed) {
|
||||
|
||||
fillIfEmpty(fields.address1, fallback.address1);
|
||||
fillIfEmpty(fields.city, fallback.city);
|
||||
fillIfEmpty(fields.region, fallback.region);
|
||||
await selectRegionDropdown(findRegionDropdown(), fallback.region);
|
||||
fillIfEmpty(fields.postalCode, fallback.postalCode);
|
||||
await sleep(500);
|
||||
|
||||
@@ -713,7 +847,7 @@ async function ensureStructuredAddress(seed) {
|
||||
return {
|
||||
address1: latest.address1?.value || '',
|
||||
city: latest.city?.value || '',
|
||||
region: latest.region?.value || '',
|
||||
region: getRegionDropdownValue(findRegionDropdown()) || latest.region?.value || '',
|
||||
postalCode: latest.postalCode?.value || '',
|
||||
};
|
||||
}
|
||||
@@ -759,7 +893,10 @@ async function fillPlusBillingAddress(payload = {}) {
|
||||
postalCode: '10117',
|
||||
},
|
||||
};
|
||||
const selected = await selectAddressSuggestion(seed);
|
||||
let selected = { selectedText: '' };
|
||||
if (!seed.skipAutocomplete) {
|
||||
selected = await selectAddressSuggestion(seed);
|
||||
}
|
||||
const structuredAddress = await ensureStructuredAddress(seed);
|
||||
|
||||
return {
|
||||
@@ -828,7 +965,7 @@ function inspectPlusCheckoutState() {
|
||||
addressFieldValues: {
|
||||
address1: structuredAddress.address1?.value || '',
|
||||
city: structuredAddress.city?.value || '',
|
||||
region: structuredAddress.region?.value || '',
|
||||
region: getRegionDropdownValue(findRegionDropdown()) || structuredAddress.region?.value || '',
|
||||
postalCode: structuredAddress.postalCode?.value || '',
|
||||
},
|
||||
};
|
||||
|
||||
+156
-1
@@ -171,7 +171,162 @@
|
||||
常见情况是上传身份证件。
|
||||
`PayPal` 官方帮助中心说明,通常会在 `2 个工作日` 内审核,但某些情况可能更久。
|
||||
|
||||
### 第六部分:配置 [Clash Verge](https://github.com/clash-verge-rev/clash-verge-rev) 的 `🔁 非港轮询`
|
||||
### 第六部分:0元试用 ChatGPT Plus 教程
|
||||
|
||||
本部分说明如何在已登录 ChatGPT 的状态下,通过脚本快速生成 Plus 支付链接,然后选择 PayPal 支付完成0元试用订阅。
|
||||
|
||||
#### 准备工作
|
||||
|
||||
1. 已有一个登录状态的 ChatGPT 账户。
|
||||
2. 一个可用的 PayPal 账户(参考第五部分进行注册和绑卡)。
|
||||
3. 能够接收生成的账单地址的真实地址或虚拟地址。
|
||||
4. Chrome 浏览器(推荐使用地址补全功能)。
|
||||
|
||||
#### 执行步骤
|
||||
|
||||
1. **进入 ChatGPT 并打开开发者工具**
|
||||
在已登录 ChatGPT 的页面上,按 `F12` 打开浏览器开发者工具。
|
||||
点击 `Console`(控制台)标签进入命令行界面。
|
||||
|
||||
2. **允许粘贴脚本**
|
||||
在控制台输入 `allow pasting` 并回车。
|
||||
浏览器将允许你粘贴多行脚本代码。
|
||||
|
||||
3. **粘贴并执行脚本**
|
||||
复制下方脚本代码,粘贴到控制台,然后回车执行。
|
||||
|
||||
```javascript
|
||||
(async function(){
|
||||
try {
|
||||
const t = await (await fetch("/api/auth/session")).json();
|
||||
if(!t.accessToken){
|
||||
alert("请先登录 ChatGPT!");
|
||||
return;
|
||||
}
|
||||
|
||||
// 核心1:强制使用触发 PayPal 的欧洲区参数 (DE 德国 / EUR 欧元)
|
||||
const payload = {
|
||||
entry_point: "all_plans_pricing_modal",
|
||||
plan_name: "chatgptplusplan", // Plus 的套餐名
|
||||
billing_details: {
|
||||
country: "DE", // 必须是 DE 或 FR 才能在后续页面使用 PayPal
|
||||
currency: "EUR"
|
||||
},
|
||||
checkout_ui_mode: "custom",
|
||||
promo_campaign: {
|
||||
promo_campaign_id: "plus-1-month-free", // Plus 对应优惠码
|
||||
is_coupon_from_query_param: false
|
||||
}
|
||||
};
|
||||
|
||||
const response = await fetch("https://chatgpt.com/backend-api/payments/checkout", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Authorization": "Bearer " + t.accessToken,
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if(data.checkout_session_id) {
|
||||
// 核心2:拼接 Plus 的专属支付短链
|
||||
// 如果 openai_ie 报错,可以直接把 /openai_ie 删掉,变成 "https://chatgpt.com/checkout/" + data.checkout_session_id
|
||||
const shortLink = "https://chatgpt.com/checkout/openai_ie/" + data.checkout_session_id;
|
||||
|
||||
// 弹窗让你复制这个带有 PayPal 的短链
|
||||
prompt("提取成功!这是你的 Plus 支付短链(复制保留):", shortLink);
|
||||
|
||||
// 自动跳转到该短链
|
||||
window.location.href = shortLink;
|
||||
} else {
|
||||
console.error(data);
|
||||
alert("提取失败:" + (data.detail || JSON.stringify(data)));
|
||||
}
|
||||
} catch(e) {
|
||||
alert("发生异常:" + e);
|
||||
}
|
||||
})();
|
||||
```
|
||||
|
||||
4. **复制支付短链**
|
||||
脚本执行后会弹出一个对话框,显示你的 Plus 支付短链。
|
||||
点击"确定"按钮,页面会自动跳转到支付页面。
|
||||
(可以复制这个链接备用,以防需要重新进入。)
|
||||
|
||||
5. **选择 PayPal 支付**
|
||||
页面加载完成后,你会看到 ChatGPT Plus 的 checkout 页面,标题通常为"开始免费试用 Plus"。
|
||||
在左侧付款方式中选择 `PayPal`。
|
||||
|
||||
6. **填写账单信息 - 选择国家**
|
||||
页面右侧会显示账单地址表单。
|
||||
"国家或地区" 字段会根据你当前的地址预填(通常是德国或法国)。
|
||||
如果页面显示的不是你期望的国家,可以点击下拉框更改(建议保持德国或法国,以确保支付流程顺利)。
|
||||
|
||||
7. **填写账单信息 - 输入完整名字**
|
||||
在 "全名" 字段中输入完整的英文名字(例如 `John Smith`)。
|
||||
|
||||
8. **生成和填写地址**
|
||||
在 "地址第 1 行" 字段中开始输入。
|
||||
输入城市名、州名或街道名(例如输入 `Berlin` 或 `New York`)。
|
||||
页面会弹出 Google 地址推荐列表。
|
||||
从推荐列表中选择一个地址项(建议选择第二项)。
|
||||
页面会自动回填"城市"、"州"、"邮编"等字段。
|
||||
|
||||
9. **使用虚拟地址生成工具(可选)**
|
||||
如果地址推荐没有出现或推荐的地址不满足需求,可以使用 [https://www.meiguodizhi.com/](https://www.meiguodizhi.com/) 生成虚拟地址。
|
||||
在该网站输入城市名或随机字母获取推荐地址,复制完整地址后粘贴到表单对应字段。
|
||||
|
||||
10. **完成地址填写并点击订阅**
|
||||
确认所有必填字段已填完(全名、地址、城市、州、邮编)。
|
||||
在右侧点击 "订阅" 按钮。
|
||||
页面会跳转到 PayPal 登录界面。
|
||||
|
||||
11. **PayPal 登录**
|
||||
在 PayPal 登录页输入 PayPal 账户邮箱。
|
||||
输入 PayPal 账户密码。
|
||||
点击 "登录" 按钮。
|
||||
|
||||
12. **处理浏览器通行密钥提示(如果出现)**
|
||||
如果出现 "要在无痕模式以外保存此通行密钥吗?" 的弹窗,点击 "取消"。
|
||||
|
||||
13. **关闭 PayPal 引导弹窗(如果出现)**
|
||||
如果出现 "下次登录更快捷" 或其他 PayPal 通行密钥引导弹窗,点击右上角的关闭图标。
|
||||
|
||||
14. **同意并继续**
|
||||
页面显示 "只需一次设置,结账更快捷。" 以及向 `OpenAI Ireland Limited` 付款的摘要。
|
||||
点击 "同意并继续" 按钮。
|
||||
等待页面跳转并加载完成。
|
||||
|
||||
15. **订阅成功**
|
||||
页面会回跳到 ChatGPT 或 OpenAI 的订阅确认页面。
|
||||
此时 Plus 的0元试用订阅已成功完成。
|
||||
你现在可以使用 ChatGPT Plus 的所有功能,试用期结束后会自动按月续订。
|
||||
|
||||
#### 常见问题处理
|
||||
|
||||
- **脚本执行报错 "请先登录 ChatGPT!"**
|
||||
请确认你当前已登录 ChatGPT 账户。退出重新登录后再尝试。
|
||||
|
||||
- **脚本执行返回错误信息**
|
||||
检查网络连接是否正常。如果多次尝试仍然失败,可能是当前账户不符合0元试用条件,请稍后再试。
|
||||
|
||||
- **/openai_ie 部分出现404错误**
|
||||
如果短链中的 `/openai_ie/` 无法访问,手动修改短链为:
|
||||
`https://chatgpt.com/checkout/{checkout_session_id}` (删除 `/openai_ie/`)
|
||||
然后在浏览器地址栏中重新访问。
|
||||
|
||||
- **支付页面没有显示 PayPal 选项**
|
||||
这可能是因为脚本中的 `country` 参数不是 `DE` 或 `FR`。重新运行脚本,确保国家参数为德国 (DE) 或法国 (FR)。
|
||||
|
||||
- **地址推荐没有出现**
|
||||
稍等 1-2 秒,输入框下方应该会出现地址推荐列表。如果仍未出现,尝试清空后重新输入,或直接使用虚拟地址生成网站的地址。
|
||||
|
||||
- **PayPal 登录后页面无法继续跳转**
|
||||
稍等片刻让页面加载完毕。如果长时间未响应,检查浏览器是否有弹窗被隐藏,或尝试刷新页面。
|
||||
|
||||
### 第七部分:配置 [Clash Verge](https://github.com/clash-verge-rev/clash-verge-rev) 的 `🔁 非港轮询`
|
||||
|
||||
#### 第一步:添加扩展脚本
|
||||
|
||||
|
||||
@@ -199,3 +199,57 @@ return { isPayPalPaymentMethodActive };
|
||||
paypalButton.getAttribute = (key) => (key === 'aria-selected' ? 'true' : (paypalButton.id && key === 'id' ? paypalButton.id : ''));
|
||||
assert.equal(api.isPayPalPaymentMethodActive(), true);
|
||||
});
|
||||
|
||||
test('selectRegionDropdown opens the state dropdown and clicks the matching option', async () => {
|
||||
const bundle = [
|
||||
'function throwIfStopped() {}',
|
||||
'function sleep() { return Promise.resolve(); }',
|
||||
extractFunction('isVisibleElement'),
|
||||
extractFunction('normalizeText'),
|
||||
extractFunction('getActionText'),
|
||||
extractFunction('getRegionCandidates'),
|
||||
extractFunction('matchesRegionOption'),
|
||||
extractFunction('getRegionDropdownValue'),
|
||||
extractFunction('getVisibleRegionOptions'),
|
||||
extractFunction('selectRegionDropdown'),
|
||||
].join('\n');
|
||||
|
||||
const state = { opened: false };
|
||||
const clicks = [];
|
||||
const stateDropdown = createElement({
|
||||
tagName: 'DIV',
|
||||
text: 'State New South Wales',
|
||||
attrs: {
|
||||
role: 'combobox',
|
||||
'aria-haspopup': 'listbox',
|
||||
},
|
||||
});
|
||||
const options = [
|
||||
createElement({ tagName: 'DIV', text: 'New South Wales', attrs: { role: 'option' } }),
|
||||
createElement({ tagName: 'DIV', text: 'Western Australia', attrs: { role: 'option' } }),
|
||||
];
|
||||
const documentMock = {
|
||||
querySelectorAll: (selector) => {
|
||||
if (!state.opened) return [];
|
||||
if (selector === '[role="listbox"] [role="option"]' || selector === '[role="option"]') return options;
|
||||
if (selector === 'li') return [];
|
||||
return [];
|
||||
},
|
||||
};
|
||||
const windowMock = {
|
||||
getComputedStyle: () => ({ display: 'block', visibility: 'visible' }),
|
||||
};
|
||||
|
||||
const api = new Function('window', 'document', 'Event', 'clicks', 'stateDropdown', 'state', `
|
||||
function simulateClick(el) {
|
||||
clicks.push(el);
|
||||
if (el === stateDropdown) state.opened = true;
|
||||
}
|
||||
${bundle}
|
||||
return { selectRegionDropdown };
|
||||
`)(windowMock, documentMock, Event, clicks, stateDropdown, state);
|
||||
|
||||
await api.selectRegionDropdown(stateDropdown, 'Western Australia');
|
||||
|
||||
assert.deepEqual(clicks, [stateDropdown, options[1]]);
|
||||
});
|
||||
|
||||
@@ -22,6 +22,20 @@ function createAddressSeed() {
|
||||
};
|
||||
}
|
||||
|
||||
function createAuAddressSeed() {
|
||||
return {
|
||||
countryCode: 'AU',
|
||||
query: 'Sydney NSW',
|
||||
suggestionIndex: 1,
|
||||
fallback: {
|
||||
address1: 'George Street',
|
||||
city: 'Sydney',
|
||||
region: 'New South Wales',
|
||||
postalCode: '2000',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createSuccessfulBillingResult() {
|
||||
return {
|
||||
countryText: 'Germany',
|
||||
@@ -33,7 +47,13 @@ function createSuccessfulBillingResult() {
|
||||
};
|
||||
}
|
||||
|
||||
function createExecutorHarness({ frames, stateByFrame, readyByFrame = {} }) {
|
||||
function createExecutorHarness({
|
||||
frames,
|
||||
stateByFrame,
|
||||
readyByFrame = {},
|
||||
fetchImpl = null,
|
||||
getAddressSeedForCountry = () => createAddressSeed(),
|
||||
}) {
|
||||
const api = loadPlusCheckoutBillingModule();
|
||||
const events = {
|
||||
completed: [],
|
||||
@@ -96,8 +116,9 @@ function createExecutorHarness({ frames, stateByFrame, readyByFrame = {} }) {
|
||||
},
|
||||
completeStepFromBackground: async (step, payload) => events.completed.push({ step, payload }),
|
||||
ensureContentScriptReadyOnTabUntilStopped: async (source, tabId) => events.ensuredTabs.push({ source, tabId }),
|
||||
fetch: fetchImpl,
|
||||
generateRandomName: () => ({ firstName: 'Ada', lastName: 'Lovelace' }),
|
||||
getAddressSeedForCountry: () => createAddressSeed(),
|
||||
getAddressSeedForCountry,
|
||||
getTabId: async () => null,
|
||||
isTabAlive: async () => false,
|
||||
setState: async (updates) => events.states.push(updates),
|
||||
@@ -221,6 +242,118 @@ test('Plus checkout billing uses the autocomplete iframe for address suggestions
|
||||
assert.equal(events.completed[0].step, 7);
|
||||
});
|
||||
|
||||
test('Plus checkout billing skips Google autocomplete when meiguodizhi returns a complete address', 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' },
|
||||
{ frameId: 9, url: 'https://js.stripe.com/v3/elements-inner-autocompl.html' },
|
||||
],
|
||||
stateByFrame: {
|
||||
0: { hasPayPal: false, paypalCandidates: [], hasSubscribeButton: true },
|
||||
7: { hasPayPal: true, paypalCandidates: [{ tag: 'button', text: 'PayPal' }] },
|
||||
8: { hasPayPal: false, paypalCandidates: [], billingFieldsVisible: true },
|
||||
9: { hasPayPal: false, paypalCandidates: [] },
|
||||
},
|
||||
fetchImpl: async (url, init) => {
|
||||
fetchRequests.push({ url, init });
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => ({
|
||||
status: 'ok',
|
||||
address: {
|
||||
Address: 'Rosa-Luxemburg-Strasse 40',
|
||||
City: 'Berlin',
|
||||
State: 'Berlin',
|
||||
Zip_Code: '69081',
|
||||
},
|
||||
}),
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
await executor.executePlusCheckoutBilling({});
|
||||
|
||||
const fillQueryMessage = events.messages.find((entry) => entry.message.type === 'PLUS_CHECKOUT_FILL_ADDRESS_QUERY');
|
||||
const suggestionMessage = events.messages.find((entry) => entry.message.type === 'PLUS_CHECKOUT_SELECT_ADDRESS_SUGGESTION');
|
||||
const ensureAddressMessage = events.messages.find((entry) => entry.message.type === 'PLUS_CHECKOUT_ENSURE_BILLING_ADDRESS');
|
||||
const combinedFillMessage = events.messages.find((entry) => entry.message.type === 'PLUS_CHECKOUT_FILL_BILLING_ADDRESS');
|
||||
assert.equal(fillQueryMessage, undefined);
|
||||
assert.equal(suggestionMessage, undefined);
|
||||
assert.equal(ensureAddressMessage, undefined);
|
||||
assert.equal(combinedFillMessage.frameId, 8);
|
||||
assert.equal(combinedFillMessage.message.payload.addressSeed.skipAutocomplete, true);
|
||||
assert.equal(combinedFillMessage.message.payload.addressSeed.source, 'meiguodizhi');
|
||||
assert.equal(combinedFillMessage.message.payload.addressSeed.fallback.address1, 'Rosa-Luxemburg-Strasse 40');
|
||||
assert.equal(combinedFillMessage.message.payload.addressSeed.fallback.city, 'Berlin');
|
||||
assert.equal(combinedFillMessage.message.payload.addressSeed.fallback.postalCode, '69081');
|
||||
assert.equal(fetchRequests.length, 1);
|
||||
assert.equal(fetchRequests[0].url, 'https://www.meiguodizhi.com/api/v1/dz');
|
||||
assert.deepEqual(JSON.parse(fetchRequests[0].init.body), {
|
||||
city: 'Berlin',
|
||||
path: '/de-address',
|
||||
method: 'refresh',
|
||||
});
|
||||
assert.equal(events.completed[0].step, 7);
|
||||
});
|
||||
|
||||
test('Plus checkout billing uses the detected checkout country before choosing an address seed', async () => {
|
||||
const requestedCountries = [];
|
||||
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: 'Australia',
|
||||
},
|
||||
},
|
||||
getAddressSeedForCountry: (countryValue) => {
|
||||
requestedCountries.push(countryValue);
|
||||
return /australia|au/i.test(String(countryValue || '')) ? createAuAddressSeed() : createAddressSeed();
|
||||
},
|
||||
fetchImpl: async (url, init) => {
|
||||
fetchRequests.push({ url, init });
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => ({
|
||||
status: 'ok',
|
||||
address: {
|
||||
Address: '98 Ocean Street',
|
||||
City: 'Sydney South',
|
||||
State: 'New South Wales',
|
||||
Zip_Code: '2000',
|
||||
},
|
||||
}),
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
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(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), {
|
||||
city: 'Sydney',
|
||||
path: '/au-address',
|
||||
method: 'refresh',
|
||||
});
|
||||
});
|
||||
|
||||
test('Plus checkout billing reports when the payment iframe exists but cannot receive the content script', async () => {
|
||||
const { executor } = createExecutorHarness({
|
||||
frames: [
|
||||
|
||||
Reference in New Issue
Block a user