步骤七修复
This commit is contained in:
@@ -6915,6 +6915,7 @@ const plusCheckoutCreateExecutor = self.MultiPageBackgroundPlusCheckoutCreate?.c
|
||||
});
|
||||
const plusCheckoutBillingExecutor = self.MultiPageBackgroundPlusCheckoutBilling?.createPlusCheckoutBillingExecutor({
|
||||
addLog,
|
||||
chrome,
|
||||
completeStepFromBackground,
|
||||
ensureContentScriptReadyOnTabUntilStopped,
|
||||
generateRandomName,
|
||||
|
||||
@@ -3,33 +3,287 @@
|
||||
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundPlusCheckoutBillingModule() {
|
||||
const PLUS_CHECKOUT_SOURCE = 'plus-checkout';
|
||||
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;
|
||||
|
||||
function createPlusCheckoutBillingExecutor(deps = {}) {
|
||||
const {
|
||||
addLog,
|
||||
chrome,
|
||||
completeStepFromBackground,
|
||||
ensureContentScriptReadyOnTabUntilStopped,
|
||||
generateRandomName,
|
||||
getAddressSeedForCountry,
|
||||
getTabId,
|
||||
isTabAlive,
|
||||
sendTabMessageUntilStopped,
|
||||
setState,
|
||||
sleepWithStop,
|
||||
waitForTabCompleteUntilStopped,
|
||||
waitForTabUrlMatchUntilStopped,
|
||||
} = deps;
|
||||
|
||||
function isPlusCheckoutUrl(url = '') {
|
||||
return PLUS_CHECKOUT_URL_PATTERN.test(String(url || ''));
|
||||
}
|
||||
|
||||
async function getAlivePlusCheckoutTabId(tabId) {
|
||||
if (!Number.isInteger(tabId) || tabId <= 0) {
|
||||
return null;
|
||||
}
|
||||
if (!chrome?.tabs?.get) {
|
||||
return tabId;
|
||||
}
|
||||
const tab = await chrome.tabs.get(tabId).catch(() => null);
|
||||
return tab && isPlusCheckoutUrl(tab.url) ? tabId : null;
|
||||
}
|
||||
|
||||
async function getCurrentPlusCheckoutTabId() {
|
||||
if (!chrome?.tabs?.query) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const activeTabs = await chrome.tabs.query({ active: true, currentWindow: true }).catch(() => []);
|
||||
const activeCheckoutTab = activeTabs.find((tab) => Number.isInteger(tab?.id) && isPlusCheckoutUrl(tab.url));
|
||||
if (activeCheckoutTab) {
|
||||
return activeCheckoutTab.id;
|
||||
}
|
||||
|
||||
const checkoutTabs = await chrome.tabs.query({ url: 'https://chatgpt.com/checkout/*' }).catch(() => []);
|
||||
const checkoutTab = checkoutTabs.find((tab) => Number.isInteger(tab?.id) && isPlusCheckoutUrl(tab.url));
|
||||
return checkoutTab?.id || null;
|
||||
}
|
||||
|
||||
async function getCheckoutFrames(tabId) {
|
||||
if (!chrome?.webNavigation?.getAllFrames) {
|
||||
return [{ frameId: 0, url: '' }];
|
||||
}
|
||||
const frames = await chrome.webNavigation.getAllFrames({ tabId }).catch(() => null);
|
||||
if (!Array.isArray(frames) || !frames.length) {
|
||||
return [{ frameId: 0, url: '' }];
|
||||
}
|
||||
return frames
|
||||
.filter((frame) => Number.isInteger(frame?.frameId))
|
||||
.sort((left, right) => Number(left.frameId) - Number(right.frameId));
|
||||
}
|
||||
|
||||
async function pingCheckoutFrame(tabId, frameId) {
|
||||
try {
|
||||
const pong = await chrome.tabs.sendMessage(tabId, {
|
||||
type: 'PING',
|
||||
source: 'background',
|
||||
payload: {},
|
||||
}, {
|
||||
frameId: Number.isInteger(frameId) ? frameId : 0,
|
||||
});
|
||||
return Boolean(pong?.ok && (!pong.source || pong.source === PLUS_CHECKOUT_SOURCE));
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function ensurePlusCheckoutFrameReady(tabId, frameId) {
|
||||
if (await pingCheckoutFrame(tabId, frameId)) {
|
||||
return true;
|
||||
}
|
||||
if (!chrome?.scripting?.executeScript) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
await chrome.scripting.executeScript({
|
||||
target: { tabId, frameIds: [frameId] },
|
||||
func: (injectedSource) => {
|
||||
window.__MULTIPAGE_SOURCE = injectedSource;
|
||||
},
|
||||
args: [PLUS_CHECKOUT_SOURCE],
|
||||
});
|
||||
await chrome.scripting.executeScript({
|
||||
target: { tabId, frameIds: [frameId] },
|
||||
files: PLUS_CHECKOUT_INJECT_FILES,
|
||||
});
|
||||
} catch {
|
||||
// If the frame was already injected or navigated mid-injection, ping once more below.
|
||||
}
|
||||
|
||||
await sleepWithStop(PLUS_CHECKOUT_FRAME_READY_DELAY_MS);
|
||||
return await pingCheckoutFrame(tabId, frameId);
|
||||
}
|
||||
|
||||
async function ensurePlusCheckoutFramesReady(tabId, frames) {
|
||||
const checkedFrames = [];
|
||||
for (const frame of frames) {
|
||||
const ready = await ensurePlusCheckoutFrameReady(tabId, frame.frameId);
|
||||
checkedFrames.push({ ...frame, ready });
|
||||
}
|
||||
return checkedFrames;
|
||||
}
|
||||
|
||||
async function sendFrameMessage(tabId, frameId, message) {
|
||||
return chrome.tabs.sendMessage(tabId, message, {
|
||||
frameId: Number.isInteger(frameId) ? frameId : 0,
|
||||
});
|
||||
}
|
||||
|
||||
async function inspectCheckoutFrame(tabId, frame) {
|
||||
if (frame.ready === false) {
|
||||
return { frame, error: 'content-script-not-ready' };
|
||||
}
|
||||
try {
|
||||
const result = await sendFrameMessage(tabId, frame.frameId, {
|
||||
type: 'PLUS_CHECKOUT_GET_STATE',
|
||||
source: 'background',
|
||||
payload: {},
|
||||
});
|
||||
if (result?.error) {
|
||||
return { frame, error: result.error };
|
||||
}
|
||||
return { frame, result: result || {} };
|
||||
} catch (error) {
|
||||
return { frame, error: error?.message || String(error || '') };
|
||||
}
|
||||
}
|
||||
|
||||
function isPaymentFrameUrl(url = '') {
|
||||
return /elements-inner-payment|componentName=payment/i.test(String(url || ''));
|
||||
}
|
||||
|
||||
function isAddressFrameUrl(url = '') {
|
||||
return /elements-inner-address|componentName=address/i.test(String(url || ''));
|
||||
}
|
||||
|
||||
function isAutocompleteFrameUrl(url = '') {
|
||||
return /elements-inner-autocompl/i.test(String(url || ''));
|
||||
}
|
||||
|
||||
function buildFrameSummary(inspections) {
|
||||
return inspections
|
||||
.map((item) => {
|
||||
const flags = [];
|
||||
if (item.result?.hasPayPal) flags.push('paypal');
|
||||
if (item.result?.billingFieldsVisible) flags.push('billing');
|
||||
if (item.result?.hasSubscribeButton) flags.push('subscribe');
|
||||
if (!flags.length && item.error) flags.push(item.error);
|
||||
if (!flags.length) flags.push('no-match');
|
||||
return `${item.frame.frameId}:${item.frame.url || 'about:blank'}:${flags.join(',')}`;
|
||||
})
|
||||
.slice(0, 8)
|
||||
.join(' | ');
|
||||
}
|
||||
|
||||
async function inspectCheckoutFrames(tabId, frames) {
|
||||
const inspections = [];
|
||||
for (const frame of frames) {
|
||||
const inspection = await inspectCheckoutFrame(tabId, frame);
|
||||
inspections.push(inspection);
|
||||
}
|
||||
return inspections;
|
||||
}
|
||||
|
||||
function pickPaymentFrame(inspections) {
|
||||
return inspections.find((item) => item.result?.hasPayPal || item.result?.paypalCandidates?.length)
|
||||
|| inspections.find((item) => isPaymentFrameUrl(item.frame.url))
|
||||
|| null;
|
||||
}
|
||||
|
||||
function pickBillingFrame(inspections) {
|
||||
return inspections.find((item) => item.result?.billingFieldsVisible)
|
||||
|| inspections.find((item) => isAddressFrameUrl(item.frame.url))
|
||||
|| null;
|
||||
}
|
||||
|
||||
function pickSubscribeFrame(inspections) {
|
||||
return inspections.find((item) => item.result?.hasSubscribeButton)
|
||||
|| inspections.find((item) => item.frame.frameId === 0)
|
||||
|| null;
|
||||
}
|
||||
|
||||
async function getReadyCheckoutFrames(tabId) {
|
||||
return ensurePlusCheckoutFramesReady(tabId, await getCheckoutFrames(tabId));
|
||||
}
|
||||
|
||||
async function resolveOptionalFrameByUrl(tabId, predicate) {
|
||||
const frames = await getCheckoutFrames(tabId);
|
||||
const frame = frames.find((item) => predicate(item.url));
|
||||
if (!frame) {
|
||||
return null;
|
||||
}
|
||||
const ready = await ensurePlusCheckoutFrameReady(tabId, frame.frameId);
|
||||
return {
|
||||
frame,
|
||||
ready,
|
||||
};
|
||||
}
|
||||
|
||||
async function resolvePaymentFrame(tabId, frames) {
|
||||
const inspections = await inspectCheckoutFrames(tabId, frames);
|
||||
const picked = pickPaymentFrame(inspections);
|
||||
if (picked) {
|
||||
return {
|
||||
frameId: picked.frame.frameId,
|
||||
frameUrl: picked.frame.url || '',
|
||||
ready: picked.frame.ready !== false,
|
||||
inspections,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
frameId: null,
|
||||
frameUrl: '',
|
||||
inspections,
|
||||
};
|
||||
}
|
||||
|
||||
async function waitForBillingFrame(tabId) {
|
||||
while (true) {
|
||||
const frames = await getReadyCheckoutFrames(tabId);
|
||||
const inspections = await inspectCheckoutFrames(tabId, frames);
|
||||
const picked = pickBillingFrame(inspections);
|
||||
if (picked) {
|
||||
return {
|
||||
frameId: picked.frame.frameId,
|
||||
frameUrl: picked.frame.url || '',
|
||||
ready: picked.frame.ready !== false,
|
||||
inspections,
|
||||
};
|
||||
}
|
||||
await sleepWithStop(250);
|
||||
}
|
||||
}
|
||||
|
||||
async function waitForSubscribeFrame(tabId, candidateFrames) {
|
||||
const frames = candidateFrames.length ? candidateFrames : [{ frameId: 0, url: '' }];
|
||||
while (true) {
|
||||
const readyFrames = await ensurePlusCheckoutFramesReady(tabId, frames);
|
||||
const inspections = await inspectCheckoutFrames(tabId, readyFrames);
|
||||
const picked = pickSubscribeFrame(inspections);
|
||||
if (picked) {
|
||||
return picked.frame;
|
||||
}
|
||||
await sleepWithStop(250);
|
||||
}
|
||||
}
|
||||
|
||||
async function getCheckoutTabId(state = {}) {
|
||||
const registeredTabId = await getTabId(PLUS_CHECKOUT_SOURCE);
|
||||
if (registeredTabId && await isTabAlive(PLUS_CHECKOUT_SOURCE)) {
|
||||
return registeredTabId;
|
||||
const aliveRegisteredTabId = await getAlivePlusCheckoutTabId(registeredTabId);
|
||||
if (aliveRegisteredTabId) {
|
||||
return aliveRegisteredTabId;
|
||||
}
|
||||
}
|
||||
const storedTabId = Number(state.plusCheckoutTabId) || 0;
|
||||
if (storedTabId) {
|
||||
return storedTabId;
|
||||
const aliveStoredTabId = await getAlivePlusCheckoutTabId(storedTabId);
|
||||
if (aliveStoredTabId) {
|
||||
return aliveStoredTabId;
|
||||
}
|
||||
}
|
||||
throw new Error('步骤 7:未找到 Plus Checkout 标签页,请先完成步骤 6。');
|
||||
const currentCheckoutTabId = await getCurrentPlusCheckoutTabId();
|
||||
if (currentCheckoutTabId) {
|
||||
await addLog('步骤 7:检测到当前已在 Plus Checkout 页面,直接接管当前标签页。', 'info');
|
||||
return currentCheckoutTabId;
|
||||
}
|
||||
throw new Error('步骤 7:未找到 Plus Checkout 标签页。请先打开 Plus Checkout 页面,或完成步骤 6。');
|
||||
}
|
||||
|
||||
async function executePlusCheckoutBilling(state = {}) {
|
||||
@@ -43,6 +297,19 @@
|
||||
injectSource: PLUS_CHECKOUT_SOURCE,
|
||||
logMessage: '步骤 7:Checkout 页面仍在加载,等待账单填写脚本就绪...',
|
||||
});
|
||||
const readyFrames = await getReadyCheckoutFrames(tabId);
|
||||
const paymentFrame = await resolvePaymentFrame(tabId, readyFrames);
|
||||
if (paymentFrame.frameId === null) {
|
||||
const frameSummary = buildFrameSummary(paymentFrame.inspections);
|
||||
throw new Error(`步骤 7:未在主页面或 iframe 中发现 PayPal DOM,无法自动切换付款方式。frame 摘要:${frameSummary}`);
|
||||
}
|
||||
if (!paymentFrame.ready) {
|
||||
throw new Error(`步骤 7:已定位到 PayPal 所在 iframe(frameId=${paymentFrame.frameId}),但账单脚本无法注入该 iframe。请提供该 iframe 的控制台结构或截图。`);
|
||||
}
|
||||
|
||||
if (paymentFrame.frameId !== 0) {
|
||||
await addLog(`步骤 7:PayPal 位于 checkout iframe(frameId=${paymentFrame.frameId}),将改为在该 frame 内操作。`, 'info');
|
||||
}
|
||||
|
||||
const randomName = generateRandomName();
|
||||
const fullName = [randomName.firstName, randomName.lastName].filter(Boolean).join(' ');
|
||||
@@ -53,18 +320,99 @@
|
||||
throw new Error('步骤 7:未找到可用的本地账单地址种子。');
|
||||
}
|
||||
|
||||
await addLog(`步骤 7:正在选择 PayPal 并填写账单地址(${addressSeed.countryCode} / ${addressSeed.query})...`, 'info');
|
||||
const result = await sendTabMessageUntilStopped(tabId, PLUS_CHECKOUT_SOURCE, {
|
||||
type: 'FILL_PLUS_BILLING_AND_SUBMIT',
|
||||
await addLog('步骤 7:正在切换 PayPal 付款方式...', 'info');
|
||||
const paymentResult = await sendFrameMessage(tabId, paymentFrame.frameId, {
|
||||
type: 'PLUS_CHECKOUT_SELECT_PAYPAL',
|
||||
source: 'background',
|
||||
payload: {
|
||||
fullName,
|
||||
addressSeed,
|
||||
},
|
||||
payload: {},
|
||||
});
|
||||
if (paymentResult?.error) {
|
||||
throw new Error(paymentResult.error);
|
||||
}
|
||||
|
||||
if (result?.error) {
|
||||
throw new Error(result.error);
|
||||
const billingFrame = await waitForBillingFrame(tabId);
|
||||
if (!billingFrame.ready) {
|
||||
throw new Error(`步骤 7:已定位到账单地址 iframe(frameId=${billingFrame.frameId}),但账单脚本无法注入该 iframe。请提供该 iframe 的控制台结构或截图。`);
|
||||
}
|
||||
if (billingFrame.frameId !== paymentFrame.frameId) {
|
||||
await addLog(`步骤 7:账单地址位于 checkout iframe(frameId=${billingFrame.frameId}),将改为在该 frame 内填写。`, 'info');
|
||||
}
|
||||
|
||||
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 (!autocompleteFrame.ready) {
|
||||
throw new Error('步骤 7:发现 Google 地址推荐 iframe,但无法注入账单脚本。请提供该 iframe 的控制台结构。');
|
||||
}
|
||||
await addLog(`步骤 7:Google 地址推荐位于独立 iframe(frameId=${autocompleteFrame.frame.frameId}),将拆分输入与选择动作。`, 'info');
|
||||
|
||||
const queryResult = await sendFrameMessage(tabId, billingFrame.frameId, {
|
||||
type: 'PLUS_CHECKOUT_FILL_ADDRESS_QUERY',
|
||||
source: 'background',
|
||||
payload: {
|
||||
fullName,
|
||||
addressSeed,
|
||||
},
|
||||
});
|
||||
if (queryResult?.error) {
|
||||
throw new Error(queryResult.error);
|
||||
}
|
||||
|
||||
const suggestionResult = await sendFrameMessage(tabId, autocompleteFrame.frame.frameId, {
|
||||
type: 'PLUS_CHECKOUT_SELECT_ADDRESS_SUGGESTION',
|
||||
source: 'background',
|
||||
payload: {
|
||||
addressSeed,
|
||||
},
|
||||
});
|
||||
if (suggestionResult?.error) {
|
||||
throw new Error(suggestionResult.error);
|
||||
}
|
||||
|
||||
const structuredResult = await sendFrameMessage(tabId, billingFrame.frameId, {
|
||||
type: 'PLUS_CHECKOUT_ENSURE_BILLING_ADDRESS',
|
||||
source: 'background',
|
||||
payload: {
|
||||
addressSeed,
|
||||
},
|
||||
});
|
||||
if (structuredResult?.error) {
|
||||
throw new Error(structuredResult.error);
|
||||
}
|
||||
|
||||
result = {
|
||||
...structuredResult,
|
||||
selectedAddressText: suggestionResult?.selectedAddressText || '',
|
||||
};
|
||||
} else {
|
||||
result = await sendFrameMessage(tabId, billingFrame.frameId, {
|
||||
type: 'PLUS_CHECKOUT_FILL_BILLING_ADDRESS',
|
||||
source: 'background',
|
||||
payload: {
|
||||
fullName,
|
||||
addressSeed,
|
||||
},
|
||||
});
|
||||
|
||||
if (result?.error) {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
}
|
||||
|
||||
await addLog('步骤 7:账单地址已填写完成,正在定位订阅按钮...', 'info');
|
||||
const subscribeFrame = await waitForSubscribeFrame(tabId, [
|
||||
{ frameId: 0, url: '' },
|
||||
{ frameId: paymentFrame.frameId, url: paymentFrame.frameUrl || '' },
|
||||
{ frameId: billingFrame.frameId, url: billingFrame.frameUrl || '' },
|
||||
]);
|
||||
const subscribeResult = await sendFrameMessage(tabId, subscribeFrame.frameId, {
|
||||
type: 'PLUS_CHECKOUT_CLICK_SUBSCRIBE',
|
||||
source: 'background',
|
||||
payload: {},
|
||||
});
|
||||
if (subscribeResult?.error) {
|
||||
throw new Error(subscribeResult.error);
|
||||
}
|
||||
|
||||
await setState({
|
||||
|
||||
+368
-22
@@ -16,6 +16,7 @@ const PLUS_CHECKOUT_PAYLOAD = {
|
||||
is_coupon_from_query_param: false,
|
||||
},
|
||||
};
|
||||
const PAYPAL_DIAGNOSTIC_LOG_INTERVAL_MS = 5000;
|
||||
|
||||
if (document.documentElement.getAttribute(PLUS_CHECKOUT_LISTENER_SENTINEL) !== '1') {
|
||||
document.documentElement.setAttribute(PLUS_CHECKOUT_LISTENER_SENTINEL, '1');
|
||||
@@ -24,6 +25,12 @@ if (document.documentElement.getAttribute(PLUS_CHECKOUT_LISTENER_SENTINEL) !== '
|
||||
if (
|
||||
message.type === 'CREATE_PLUS_CHECKOUT'
|
||||
|| message.type === 'FILL_PLUS_BILLING_AND_SUBMIT'
|
||||
|| message.type === 'PLUS_CHECKOUT_SELECT_PAYPAL'
|
||||
|| message.type === 'PLUS_CHECKOUT_FILL_BILLING_ADDRESS'
|
||||
|| message.type === 'PLUS_CHECKOUT_FILL_ADDRESS_QUERY'
|
||||
|| message.type === 'PLUS_CHECKOUT_SELECT_ADDRESS_SUGGESTION'
|
||||
|| message.type === 'PLUS_CHECKOUT_ENSURE_BILLING_ADDRESS'
|
||||
|| message.type === 'PLUS_CHECKOUT_CLICK_SUBSCRIBE'
|
||||
|| message.type === 'PLUS_CHECKOUT_GET_STATE'
|
||||
) {
|
||||
resetStopState();
|
||||
@@ -49,6 +56,18 @@ async function handlePlusCheckoutCommand(message) {
|
||||
return createPlusCheckoutSession();
|
||||
case 'FILL_PLUS_BILLING_AND_SUBMIT':
|
||||
return fillPlusBillingAndSubmit(message.payload || {});
|
||||
case 'PLUS_CHECKOUT_SELECT_PAYPAL':
|
||||
return selectPlusPayPalPaymentMethod();
|
||||
case 'PLUS_CHECKOUT_FILL_BILLING_ADDRESS':
|
||||
return fillPlusBillingAddress(message.payload || {});
|
||||
case 'PLUS_CHECKOUT_FILL_ADDRESS_QUERY':
|
||||
return fillPlusAddressQuery(message.payload || {});
|
||||
case 'PLUS_CHECKOUT_SELECT_ADDRESS_SUGGESTION':
|
||||
return selectPlusAddressSuggestion(message.payload || {});
|
||||
case 'PLUS_CHECKOUT_ENSURE_BILLING_ADDRESS':
|
||||
return ensurePlusStructuredBillingAddress(message.payload || {});
|
||||
case 'PLUS_CHECKOUT_CLICK_SUBSCRIBE':
|
||||
return clickPlusSubscribe();
|
||||
case 'PLUS_CHECKOUT_GET_STATE':
|
||||
return inspectPlusCheckoutState();
|
||||
default:
|
||||
@@ -103,6 +122,21 @@ function getActionText(el) {
|
||||
].filter(Boolean).join(' '));
|
||||
}
|
||||
|
||||
function getSearchText(el) {
|
||||
const datasetValues = el?.dataset ? Object.values(el.dataset) : [];
|
||||
return normalizeText([
|
||||
getActionText(el),
|
||||
el?.getAttribute?.('alt'),
|
||||
el?.getAttribute?.('role'),
|
||||
el?.getAttribute?.('data-testid'),
|
||||
el?.getAttribute?.('src'),
|
||||
el?.getAttribute?.('href'),
|
||||
el?.getAttribute?.('xlink:href'),
|
||||
typeof el?.className === 'string' ? el.className : el?.getAttribute?.('class'),
|
||||
...datasetValues,
|
||||
].filter(Boolean).join(' '));
|
||||
}
|
||||
|
||||
function getFieldText(el) {
|
||||
const id = el?.id || '';
|
||||
const labels = [];
|
||||
@@ -123,6 +157,13 @@ function getFieldText(el) {
|
||||
].filter(Boolean).join(' '));
|
||||
}
|
||||
|
||||
function getCombinedSearchText(el) {
|
||||
return normalizeText([
|
||||
getSearchText(el),
|
||||
getFieldText(el),
|
||||
].filter(Boolean).join(' '));
|
||||
}
|
||||
|
||||
function getVisibleControls(selector) {
|
||||
return Array.from(document.querySelectorAll(selector)).filter(isVisibleElement);
|
||||
}
|
||||
@@ -161,6 +202,179 @@ function findInputByFieldText(patterns, options = {}) {
|
||||
}) || null;
|
||||
}
|
||||
|
||||
function isDocumentLevelContainer(el) {
|
||||
return !el
|
||||
|| el === document.documentElement
|
||||
|| el === document.body
|
||||
|| ['HTML', 'BODY', 'MAIN'].includes(el.tagName);
|
||||
}
|
||||
|
||||
function isPaymentCardSized(el) {
|
||||
if (!isVisibleElement(el) || isDocumentLevelContainer(el)) return false;
|
||||
const rect = el.getBoundingClientRect();
|
||||
const maxWidth = Math.max(320, Math.min(window.innerWidth * 0.95, 900));
|
||||
const maxHeight = Math.max(140, Math.min(window.innerHeight * 0.45, 320));
|
||||
return rect.width >= 64
|
||||
&& rect.height >= 28
|
||||
&& rect.width <= maxWidth
|
||||
&& rect.height <= maxHeight;
|
||||
}
|
||||
|
||||
function findInteractiveAncestor(el) {
|
||||
let current = el;
|
||||
for (let depth = 0; current && depth < 8; depth += 1, current = current.parentElement) {
|
||||
if (!isVisibleElement(current) || isDocumentLevelContainer(current)) continue;
|
||||
if (current.matches?.('button, a, label, [role="button"], [role="radio"], input[type="radio"], [tabindex]')) {
|
||||
return current;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function findPaymentCardAncestor(el, pattern) {
|
||||
let current = el;
|
||||
for (let depth = 0; current && depth < 8; depth += 1, current = current.parentElement) {
|
||||
if (!isVisibleElement(current)) continue;
|
||||
if (isDocumentLevelContainer(current)) break;
|
||||
const text = getSearchText(current);
|
||||
if (pattern.test(text) && isPaymentCardSized(current)) {
|
||||
return current;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function getAncestorChainSummary(el, limit = 6) {
|
||||
const chain = [];
|
||||
let current = el;
|
||||
for (let depth = 0; current && depth < limit; depth += 1, current = current.parentElement) {
|
||||
if (isDocumentLevelContainer(current)) break;
|
||||
const rect = current.getBoundingClientRect();
|
||||
chain.push({
|
||||
tag: String(current.tagName || '').toLowerCase(),
|
||||
role: current.getAttribute?.('role') || '',
|
||||
id: current.id || '',
|
||||
className: typeof current.className === 'string' ? current.className.slice(0, 120) : '',
|
||||
testId: current.getAttribute?.('data-testid') || '',
|
||||
ariaLabel: current.getAttribute?.('aria-label') || '',
|
||||
ariaChecked: current.getAttribute?.('aria-checked') || '',
|
||||
ariaSelected: current.getAttribute?.('aria-selected') || '',
|
||||
rect: `${Math.round(rect.width)}x${Math.round(rect.height)}`,
|
||||
text: getCombinedSearchText(current).slice(0, 180),
|
||||
});
|
||||
}
|
||||
return chain;
|
||||
}
|
||||
|
||||
function getPayPalSearchCandidates() {
|
||||
const selector = [
|
||||
'button',
|
||||
'a',
|
||||
'label',
|
||||
'[role="button"]',
|
||||
'[role="radio"]',
|
||||
'input[type="radio"]',
|
||||
'[tabindex]',
|
||||
'[data-testid]',
|
||||
'[aria-label]',
|
||||
'[title]',
|
||||
'img',
|
||||
'svg',
|
||||
'span',
|
||||
'div',
|
||||
].join(', ');
|
||||
|
||||
return getVisibleControls(selector)
|
||||
.filter((el) => /paypal/i.test(getCombinedSearchText(el)))
|
||||
.sort((left, right) => {
|
||||
const leftRect = left.getBoundingClientRect();
|
||||
const rightRect = right.getBoundingClientRect();
|
||||
return (leftRect.width * leftRect.height) - (rightRect.width * rightRect.height);
|
||||
});
|
||||
}
|
||||
|
||||
function findPayPalPaymentMethodTarget() {
|
||||
const paypalPattern = /paypal/i;
|
||||
const directClickable = findClickableByText([paypalPattern]);
|
||||
if (directClickable) {
|
||||
return directClickable;
|
||||
}
|
||||
|
||||
const radios = getVisibleControls('input[type="radio"], [role="radio"]');
|
||||
const paypalRadio = radios.find((el) => paypalPattern.test(getCombinedSearchText(el)));
|
||||
if (paypalRadio) {
|
||||
return paypalRadio;
|
||||
}
|
||||
|
||||
const candidates = getPayPalSearchCandidates();
|
||||
for (const candidate of candidates) {
|
||||
const interactive = findInteractiveAncestor(candidate);
|
||||
if (interactive && paypalPattern.test(getCombinedSearchText(interactive))) {
|
||||
return interactive;
|
||||
}
|
||||
const card = findPaymentCardAncestor(candidate, paypalPattern);
|
||||
if (card) {
|
||||
return card;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function summarizeElementForDebug(el) {
|
||||
if (!el) return null;
|
||||
const rect = el.getBoundingClientRect();
|
||||
return {
|
||||
tag: String(el.tagName || '').toLowerCase(),
|
||||
role: el.getAttribute?.('role') || '',
|
||||
text: getSearchText(el).slice(0, 160),
|
||||
rect: `${Math.round(rect.width)}x${Math.round(rect.height)}`,
|
||||
chain: getAncestorChainSummary(el, 3),
|
||||
};
|
||||
}
|
||||
|
||||
function getPayPalCandidateSummaries(limit = 6) {
|
||||
return getPayPalSearchCandidates()
|
||||
.map(summarizeElementForDebug)
|
||||
.filter(Boolean)
|
||||
.slice(0, limit);
|
||||
}
|
||||
|
||||
function getPaymentTextPreview(limit = 10) {
|
||||
const seen = new Set();
|
||||
const pattern = /paypal|card|payment|billing|subscribe|pay|银行卡|付款|支付|账单|订阅/i;
|
||||
return getVisibleControls('button, a, label, [role="button"], [role="radio"], input[type="radio"], input[type="button"], input[type="submit"], [data-testid]')
|
||||
.map((el) => getCombinedSearchText(el))
|
||||
.filter((text) => text && pattern.test(text))
|
||||
.map((text) => text.slice(0, 180))
|
||||
.filter((text) => {
|
||||
if (seen.has(text)) return false;
|
||||
seen.add(text);
|
||||
return true;
|
||||
})
|
||||
.slice(0, limit);
|
||||
}
|
||||
|
||||
function getPayPalDiagnostics(reason = '') {
|
||||
return {
|
||||
reason,
|
||||
url: location.href,
|
||||
readyState: document.readyState,
|
||||
paypalCandidates: getPayPalCandidateSummaries(),
|
||||
paymentTextPreview: getPaymentTextPreview(),
|
||||
cardFieldsVisible: hasCreditCardFields(),
|
||||
billingFieldsVisible: hasBillingAddressFields(),
|
||||
};
|
||||
}
|
||||
|
||||
function writePayPalDiagnostics(reason, level = 'info') {
|
||||
const diagnostics = getPayPalDiagnostics(reason);
|
||||
const writer = typeof console[level] === 'function' ? console[level] : console.info;
|
||||
writer.call(console, '[MultiPage:plus-checkout] PayPal diagnostics', diagnostics);
|
||||
log(`Plus Checkout:${reason}。PayPal 候选 ${diagnostics.paypalCandidates.length} 个,银行卡字段${diagnostics.cardFieldsVisible ? '仍可见' : '不可见'}。`, level === 'error' ? 'error' : 'warn');
|
||||
return diagnostics;
|
||||
}
|
||||
|
||||
async function createPlusCheckoutSession() {
|
||||
await waitForDocumentComplete();
|
||||
log('Plus:正在读取 ChatGPT 登录会话...');
|
||||
@@ -199,23 +413,42 @@ async function createPlusCheckoutSession() {
|
||||
}
|
||||
|
||||
async function selectPayPalPaymentMethod() {
|
||||
const paypalPattern = /paypal/i;
|
||||
const existingSelected = findClickableByText([/paypal/i]);
|
||||
if (existingSelected) {
|
||||
simulateClick(existingSelected);
|
||||
await sleep(600);
|
||||
return true;
|
||||
let lastDiagnosticsAt = 0;
|
||||
const target = await waitUntil(() => {
|
||||
const currentTarget = findPayPalPaymentMethodTarget();
|
||||
if (currentTarget) {
|
||||
return currentTarget;
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
if (!lastDiagnosticsAt || now - lastDiagnosticsAt >= PAYPAL_DIAGNOSTIC_LOG_INTERVAL_MS) {
|
||||
lastDiagnosticsAt = now;
|
||||
writePayPalDiagnostics('正在等待可点击的 PayPal 付款方式', 'warn');
|
||||
}
|
||||
return null;
|
||||
}, {
|
||||
label: 'PayPal 付款方式',
|
||||
intervalMs: 250,
|
||||
});
|
||||
console.info('[MultiPage:plus-checkout] PayPal target selected', summarizeElementForDebug(target));
|
||||
simulateClick(target);
|
||||
await sleep(1000);
|
||||
|
||||
if (!isPayPalPaymentMethodActive()) {
|
||||
const diagnostics = writePayPalDiagnostics('点击 PayPal 后页面仍未进入 PayPal 账单表单', 'error');
|
||||
throw new Error(`Plus Checkout:已尝试点击 PayPal,但页面未切换到 PayPal 表单。请提供控制台 PayPal diagnostics 结构。候选数量:${diagnostics.paypalCandidates.length},银行卡字段仍可见:${diagnostics.cardFieldsVisible ? '是' : '否'}。`);
|
||||
}
|
||||
|
||||
const radios = getVisibleControls('input[type="radio"], [role="radio"]');
|
||||
const paypalRadio = radios.find((el) => paypalPattern.test(getFieldText(el)));
|
||||
if (paypalRadio) {
|
||||
simulateClick(paypalRadio);
|
||||
await sleep(600);
|
||||
return true;
|
||||
}
|
||||
log('Plus Checkout:已确认 PayPal 付款方式生效。');
|
||||
return true;
|
||||
}
|
||||
|
||||
throw new Error('Plus Checkout:未找到 PayPal 付款方式。');
|
||||
async function selectPlusPayPalPaymentMethod() {
|
||||
await waitForDocumentComplete();
|
||||
await selectPayPalPaymentMethod();
|
||||
return {
|
||||
paymentSelected: true,
|
||||
};
|
||||
}
|
||||
|
||||
async function fillFullName(fullName) {
|
||||
@@ -251,7 +484,7 @@ function readCountryText() {
|
||||
|
||||
function isLikelyAddressSearchInput(input) {
|
||||
const text = getFieldText(input);
|
||||
if (/name|email|e-mail|phone|tel|password|coupon|promo|country|region|postal|zip|city|state|province|全名|姓名|邮箱|电话|密码|国家|地区|邮编|城市|省|州/i.test(text)) {
|
||||
if (/name|email|e-mail|phone|tel|password|coupon|promo|country|region|postal|zip|city|state|province|card|card\s*number|expiry|expiration|security|cvc|cvv|cc-|全名|姓名|邮箱|电话|密码|国家|地区|邮编|城市|省|州|银行卡|卡号|有效期|安全码/i.test(text)) {
|
||||
return false;
|
||||
}
|
||||
if (/address|street|billing|search|line\s*1|地址|街道|账单/i.test(text)) {
|
||||
@@ -260,6 +493,52 @@ function isLikelyAddressSearchInput(input) {
|
||||
return false;
|
||||
}
|
||||
|
||||
function hasCreditCardFields() {
|
||||
return getVisibleTextInputs().some((input) => {
|
||||
const text = getFieldText(input);
|
||||
return /card\s*number|card|expiry|expiration|security\s*code|cvc|cvv|银行卡|卡号|有效期|安全码/i.test(text);
|
||||
});
|
||||
}
|
||||
|
||||
function hasBillingAddressFields() {
|
||||
return getVisibleTextInputs().some((input) => {
|
||||
const text = getFieldText(input);
|
||||
return /address|street|billing|line\s*1|地址|街道|账单/i.test(text)
|
||||
&& !/card\s*number|card|expiry|expiration|security|cvc|cvv|银行卡|卡号|有效期|安全码/i.test(text);
|
||||
});
|
||||
}
|
||||
|
||||
function hasSelectedPayPalControl() {
|
||||
const paypalPattern = /paypal/i;
|
||||
const candidates = getPayPalSearchCandidates();
|
||||
return candidates.some((candidate) => {
|
||||
let current = candidate;
|
||||
for (let depth = 0; current && depth < 6; depth += 1, current = current.parentElement) {
|
||||
if (isDocumentLevelContainer(current)) break;
|
||||
if (!paypalPattern.test(getCombinedSearchText(current))) continue;
|
||||
const className = typeof current.className === 'string' ? current.className : current.getAttribute?.('class') || '';
|
||||
if (
|
||||
current.checked === true
|
||||
|| current.getAttribute?.('aria-checked') === 'true'
|
||||
|| current.getAttribute?.('aria-selected') === 'true'
|
||||
|| current.getAttribute?.('data-state') === 'checked'
|
||||
|| current.getAttribute?.('data-selected') === 'true'
|
||||
|| /\b(selected|checked|active)\b/i.test(className)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
function isPayPalPaymentMethodActive() {
|
||||
if (hasSelectedPayPalControl()) {
|
||||
return true;
|
||||
}
|
||||
return getPayPalSearchCandidates().length > 0 && !hasCreditCardFields();
|
||||
}
|
||||
|
||||
async function findAddressSearchInput() {
|
||||
return waitUntil(() => {
|
||||
const direct = findInputByFieldText([
|
||||
@@ -302,10 +581,11 @@ function getAddressSuggestions() {
|
||||
}
|
||||
|
||||
async function selectAddressSuggestion(seed) {
|
||||
const addressInput = await findAddressSearchInput();
|
||||
fillInput(addressInput, seed.query || 'Berlin Mitte');
|
||||
await sleep(800);
|
||||
await fillAddressQuery(seed);
|
||||
return clickAddressSuggestion(seed);
|
||||
}
|
||||
|
||||
async function clickAddressSuggestion(seed = {}) {
|
||||
const suggestions = await waitUntil(() => {
|
||||
const options = getAddressSuggestions();
|
||||
return options.length ? options : null;
|
||||
@@ -327,6 +607,15 @@ async function selectAddressSuggestion(seed) {
|
||||
};
|
||||
}
|
||||
|
||||
async function fillAddressQuery(seed = {}) {
|
||||
const addressInput = await findAddressSearchInput();
|
||||
fillInput(addressInput, seed.query || 'Berlin Mitte');
|
||||
await sleep(800);
|
||||
return {
|
||||
filled: true,
|
||||
};
|
||||
}
|
||||
|
||||
function getStructuredAddressFields() {
|
||||
const address1 = findInputByFieldText([
|
||||
/address\s*(?:line)?\s*1|street/i,
|
||||
@@ -404,6 +693,24 @@ function findSubscribeButton() {
|
||||
async function fillPlusBillingAndSubmit(payload = {}) {
|
||||
await waitForDocumentComplete();
|
||||
await selectPayPalPaymentMethod();
|
||||
const billingResult = await fillPlusBillingAddress(payload);
|
||||
|
||||
if (payload.skipSubmit) {
|
||||
return {
|
||||
...billingResult,
|
||||
submitted: false,
|
||||
};
|
||||
}
|
||||
|
||||
await clickPlusSubscribe();
|
||||
return {
|
||||
...billingResult,
|
||||
submitted: true,
|
||||
};
|
||||
}
|
||||
|
||||
async function fillPlusBillingAddress(payload = {}) {
|
||||
await waitForDocumentComplete();
|
||||
await fillFullName(payload.fullName || '');
|
||||
|
||||
const countryText = readCountryText();
|
||||
@@ -420,6 +727,43 @@ async function fillPlusBillingAndSubmit(payload = {}) {
|
||||
const selected = await selectAddressSuggestion(seed);
|
||||
const structuredAddress = await ensureStructuredAddress(seed);
|
||||
|
||||
return {
|
||||
countryText,
|
||||
selectedAddressText: selected.selectedText,
|
||||
structuredAddress,
|
||||
};
|
||||
}
|
||||
|
||||
async function fillPlusAddressQuery(payload = {}) {
|
||||
await waitForDocumentComplete();
|
||||
await fillFullName(payload.fullName || '');
|
||||
const seed = payload.addressSeed || {};
|
||||
await fillAddressQuery(seed);
|
||||
return {
|
||||
countryText: readCountryText(),
|
||||
queryFilled: true,
|
||||
};
|
||||
}
|
||||
|
||||
async function selectPlusAddressSuggestion(payload = {}) {
|
||||
await waitForDocumentComplete();
|
||||
const selected = await clickAddressSuggestion(payload.addressSeed || {});
|
||||
return {
|
||||
selectedAddressText: selected.selectedText,
|
||||
suggestionIndex: selected.suggestionIndex,
|
||||
};
|
||||
}
|
||||
|
||||
async function ensurePlusStructuredBillingAddress(payload = {}) {
|
||||
await waitForDocumentComplete();
|
||||
const structuredAddress = await ensureStructuredAddress(payload.addressSeed || {});
|
||||
return {
|
||||
countryText: readCountryText(),
|
||||
structuredAddress,
|
||||
};
|
||||
}
|
||||
|
||||
async function clickPlusSubscribe() {
|
||||
const subscribeButton = await waitUntil(() => {
|
||||
const button = findSubscribeButton();
|
||||
return button && isEnabledControl(button) ? button : null;
|
||||
@@ -430,9 +774,7 @@ async function fillPlusBillingAndSubmit(payload = {}) {
|
||||
|
||||
simulateClick(subscribeButton);
|
||||
return {
|
||||
countryText,
|
||||
selectedAddressText: selected.selectedText,
|
||||
structuredAddress,
|
||||
clicked: true,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -442,7 +784,11 @@ function inspectPlusCheckoutState() {
|
||||
url: location.href,
|
||||
readyState: document.readyState,
|
||||
countryText: readCountryText(),
|
||||
hasPayPal: Boolean(findClickableByText([/paypal/i])),
|
||||
hasPayPal: Boolean(findPayPalPaymentMethodTarget()),
|
||||
paypalCandidates: getPayPalCandidateSummaries(),
|
||||
paymentTextPreview: getPaymentTextPreview(),
|
||||
cardFieldsVisible: hasCreditCardFields(),
|
||||
billingFieldsVisible: hasBillingAddressFields(),
|
||||
hasSubscribeButton: Boolean(findSubscribeButton()),
|
||||
addressFieldValues: {
|
||||
address1: structuredAddress.address1?.value || '',
|
||||
|
||||
+74
-2
@@ -1,4 +1,6 @@
|
||||
本教程用于说明相关项目地址、扩展更新方式、`QQ 邮箱`切换邮箱的使用方法,以及 `Clash Verge` 的 `🔁 非港轮询` 配置方法。
|
||||
# Codex 注册扩展相关项目、更新、邮箱切换、PayPal 与 Clash Verge 配置教程
|
||||
|
||||
本教程用于说明相关项目地址、扩展更新方式、`Cloudflare Temp Email` 与 `QQ 邮箱` 的使用方法、`PayPal` 注册绑卡流程,以及 `Clash Verge` 的 `🔁 非港轮询` 配置方法。
|
||||
|
||||
## 适用场景
|
||||
|
||||
@@ -6,6 +8,7 @@
|
||||
- 已经安装本扩展,想更新到最新版本
|
||||
- 需要把 `Cloudflare Temp Email` 用作 `邮箱生成` 或 `邮箱服务`
|
||||
- 需要临时切换 `QQ 邮箱` 地址继续使用
|
||||
- 需要注册并使用 `PayPal` 个人账户
|
||||
- 需要在 `Clash Verge` 中启用 `🔁 非港轮询`
|
||||
|
||||
## 准备内容
|
||||
@@ -15,6 +18,8 @@
|
||||
- 可以打开浏览器的 `扩展程序管理` 页面
|
||||
- 已准备好 `Cloudflare Temp Email` 后端地址;如需随机子域,域名解析也已配置完成
|
||||
- 一个可正常登录的 `QQ 邮箱`
|
||||
- 一个可正常接收短信的手机号
|
||||
- 一张可在线支付的借记卡或信用卡
|
||||
- 如需部署 `cpa`,部署环境必须可以访问 `OpenAI`
|
||||
- 已安装 `Clash Verge`,并已导入可用订阅
|
||||
|
||||
@@ -114,7 +119,59 @@
|
||||
这两个邮箱地址使用完成后,可以直接删除。
|
||||
删除后再次创建新的英文邮箱和 `Foxmail` 邮箱,即可重复注册使用。
|
||||
|
||||
### 第五部分:配置 `Clash Verge` 的 `🔁 非港轮询`
|
||||
### 第五部分:`PayPal` 注册与绑卡使用教程
|
||||
|
||||
1. 打开注册页面
|
||||
打开 `https://www.paypal.com/signin`。
|
||||
然后点击 `注册`。
|
||||
|
||||
2. 选择账户类型
|
||||
选择 `个人账户`。
|
||||
如果页面显示英文,通常对应 `Individual Account`。
|
||||
|
||||
3. 选择国家或地区
|
||||
在 `国家/地区` 中直接选择 `中国`。
|
||||
后续会绑定手机号,按中国手机号流程继续即可。
|
||||
|
||||
4. 输入手机号并继续
|
||||
按页面提示输入手机号。
|
||||
如果页面要求短信验证,就先完成短信验证再继续下一步。
|
||||
|
||||
5. 填写登录信息和个人信息
|
||||
按页面提示继续填写邮箱、密码,以及姓名、出生日期、地址和联系方式。
|
||||
`PayPal` 官方说明,中国大陆居民注册时必须使用身份证上的中文姓名,不能使用拼音。
|
||||
填完后勾选协议并完成注册。
|
||||
|
||||
6. 完成邮箱确认
|
||||
注册成功后,按页面或邮箱提示确认邮箱地址。
|
||||
如页面继续提示验证手机号,也一并完成。
|
||||
|
||||
7. 进入 `钱包` 或主页绑卡
|
||||
注册成功后,进入主页。
|
||||
然后在页面中找到 `关联卡或银行账户`,也可以直接进入 `钱包` 页面操作。
|
||||
|
||||
8. 选择关联借记卡或信用卡
|
||||
点击 `关联借记卡或信用卡`。
|
||||
如果你要先完成最基础的付款配置,优先把卡先绑上。
|
||||
|
||||
9. 填写银行卡信息
|
||||
按页面提示输入银行卡号、有效期、安全代码和账单地址。
|
||||
有效期一般在卡正面,通常写成 `10/45` 这种格式,表示 `2045 年 10 月` 到期。
|
||||
安全代码一般在卡背面,输入三位数的那个。
|
||||
如果卡背面分成两段数字,一段四位、一段三位,选择三位数的那段。
|
||||
账单地址如果页面默认信息正确,保持默认即可。
|
||||
|
||||
10. 完成绑卡并检查结果
|
||||
点击 `关联卡`。
|
||||
有时候页面会报错,但你仍然可以去 `钱包` 页面重新看一下,因为卡有可能已经绑定成功。
|
||||
|
||||
11. 检查右上角通知并完成认证
|
||||
绑卡成功后,记得查看右上角的通知图标。
|
||||
如果通知图标标红,或者页面提示账户需要认证,请按提示补交资料。
|
||||
常见情况是上传身份证件。
|
||||
`PayPal` 官方帮助中心说明,通常会在 `2 个工作日` 内审核,但某些情况可能更久。
|
||||
|
||||
### 第六部分:配置 `Clash Verge` 的 `🔁 非港轮询`
|
||||
|
||||
#### 第一步:添加扩展脚本
|
||||
|
||||
@@ -236,6 +293,18 @@ function main(config, profileName) {
|
||||
|
||||
可以。你在 `账号管理` 中创建英文邮箱和 `Foxmail` 邮箱,使用后直接删除,再重复创建即可继续使用。
|
||||
|
||||
### `PayPal` 绑卡时报错怎么办?
|
||||
|
||||
先去 `钱包` 页面再看一眼,有时虽然页面提示报错,但实际上已经绑定成功。
|
||||
如果还没有成功,优先检查账单地址是否与银行卡账单地址一致。
|
||||
`PayPal` 官方还说明,绑定新卡时会向发卡行发送最高 `1 USD` 或等值货币的临时授权验证,如果这一步被银行拒绝,绑卡也会失败。
|
||||
|
||||
### `PayPal` 右上角通知标红怎么办?
|
||||
|
||||
先点开通知查看具体要求。
|
||||
如果页面要求 `Confirm your Identity` 或账户认证,就按提示上传身份证件或其他所需材料。
|
||||
`PayPal` 官方说明,资料通常会在 `2 个工作日` 内审核,但某些情况可能更久。
|
||||
|
||||
### 为什么没有看到 `🔁 非港轮询`?
|
||||
|
||||
请先确认脚本已经完整粘贴并保存,然后回到 `代理` 页面重新查看。若仍未出现,请继续确认当前订阅可用、`代理模式` 为 `规则模式`,并且 `系统代理` 已开启。
|
||||
@@ -247,6 +316,9 @@ function main(config, profileName) {
|
||||
- 开启 `随机子域` 前,请先确认后端已经配置 `RANDOM_SUBDOMAIN_DOMAINS`,并且 Cloudflare DNS 已完成 `MX *` 设置。
|
||||
- 如需部署 `cpa`,请先确认部署环境可以访问 `OpenAI`,否则可能出现第十步认证成功但没有认证文件的问题。
|
||||
- `QQ 邮箱`切换邮箱时,建议在使用完成后及时删除,再重新创建,避免混淆当前正在使用的邮箱地址。
|
||||
- 中国大陆居民注册 `PayPal` 个人账户时,姓名请按身份证上的中文姓名填写,不要使用拼音。
|
||||
- `PayPal` 官方说明,绑定新卡时可能会向发卡行发送最高 `1 USD` 或等值货币的临时授权验证,所以完全没有余额的卡也可能因为授权失败而无法绑定。
|
||||
- 为了控制风险,更稳妥的做法是使用单独管理、余额较低的借记卡,并在绑卡后及时检查 `钱包` 页面和右上角通知。
|
||||
- 使用 `git pull` 更新扩展是最方便、最推荐的方式。
|
||||
- 在 `Clash Verge` 中粘贴脚本时,请先清空旧内容,再完整粘贴新代码并保存。
|
||||
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
|
||||
function loadPlusCheckoutBillingModule() {
|
||||
const source = fs.readFileSync('background/steps/fill-plus-checkout.js', 'utf8');
|
||||
const globalScope = {};
|
||||
return new Function('self', `${source}; return self.MultiPageBackgroundPlusCheckoutBilling;`)(globalScope);
|
||||
}
|
||||
|
||||
function createAddressSeed() {
|
||||
return {
|
||||
countryCode: 'DE',
|
||||
query: 'Berlin Mitte',
|
||||
suggestionIndex: 1,
|
||||
fallback: {
|
||||
address1: 'Unter den Linden',
|
||||
city: 'Berlin',
|
||||
region: 'Berlin',
|
||||
postalCode: '10117',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createSuccessfulBillingResult() {
|
||||
return {
|
||||
countryText: 'Germany',
|
||||
structuredAddress: {
|
||||
address1: 'Unter den Linden',
|
||||
city: 'Berlin',
|
||||
postalCode: '10117',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createExecutorHarness({ frames, stateByFrame, readyByFrame = {} }) {
|
||||
const api = loadPlusCheckoutBillingModule();
|
||||
const events = {
|
||||
completed: [],
|
||||
ensuredTabs: [],
|
||||
injectedAllFrames: false,
|
||||
logs: [],
|
||||
messages: [],
|
||||
states: [],
|
||||
waitedUrls: [],
|
||||
};
|
||||
const checkoutTab = {
|
||||
id: 42,
|
||||
url: 'https://chatgpt.com/checkout/openai_ie/cs_test',
|
||||
status: 'complete',
|
||||
};
|
||||
|
||||
const executor = api.createPlusCheckoutBillingExecutor({
|
||||
addLog: async (message, level = 'info') => events.logs.push({ message, level }),
|
||||
chrome: {
|
||||
tabs: {
|
||||
get: async (tabId) => (tabId === checkoutTab.id ? checkoutTab : null),
|
||||
query: async (queryInfo) => {
|
||||
if (queryInfo.active && queryInfo.currentWindow) {
|
||||
return [checkoutTab];
|
||||
}
|
||||
if (queryInfo.url === 'https://chatgpt.com/checkout/*') {
|
||||
return [checkoutTab];
|
||||
}
|
||||
return [];
|
||||
},
|
||||
sendMessage: async (tabId, message, options = {}) => {
|
||||
const frameId = Number.isInteger(options.frameId) ? options.frameId : 0;
|
||||
events.messages.push({ tabId, message, frameId });
|
||||
if (message.type === 'PING') {
|
||||
if (readyByFrame[frameId] === false) {
|
||||
throw new Error('No receiving end');
|
||||
}
|
||||
return { ok: true, source: 'plus-checkout' };
|
||||
}
|
||||
if (message.type === 'PLUS_CHECKOUT_GET_STATE') {
|
||||
return stateByFrame[frameId] || { hasPayPal: false, paypalCandidates: [] };
|
||||
}
|
||||
return createSuccessfulBillingResult();
|
||||
},
|
||||
},
|
||||
scripting: {
|
||||
executeScript: async (details) => {
|
||||
if (details.target?.allFrames) {
|
||||
events.injectedAllFrames = true;
|
||||
}
|
||||
},
|
||||
},
|
||||
webNavigation: {
|
||||
getAllFrames: async () => frames,
|
||||
},
|
||||
},
|
||||
completeStepFromBackground: async (step, payload) => events.completed.push({ step, payload }),
|
||||
ensureContentScriptReadyOnTabUntilStopped: async (source, tabId) => events.ensuredTabs.push({ source, tabId }),
|
||||
generateRandomName: () => ({ firstName: 'Ada', lastName: 'Lovelace' }),
|
||||
getAddressSeedForCountry: () => createAddressSeed(),
|
||||
getTabId: async () => null,
|
||||
isTabAlive: async () => false,
|
||||
setState: async (updates) => events.states.push(updates),
|
||||
sleepWithStop: async () => {},
|
||||
waitForTabCompleteUntilStopped: async () => checkoutTab,
|
||||
waitForTabUrlMatchUntilStopped: async (tabId, matcher) => {
|
||||
events.waitedUrls.push({ tabId });
|
||||
assert.equal(matcher('https://www.paypal.com/checkoutnow'), true);
|
||||
return { id: tabId, url: 'https://www.paypal.com/checkoutnow' };
|
||||
},
|
||||
});
|
||||
|
||||
return { checkoutTab, events, executor };
|
||||
}
|
||||
|
||||
test('Plus checkout billing uses the current checkout tab when step 6 did not register one', async () => {
|
||||
const { checkoutTab, events, executor } = createExecutorHarness({
|
||||
frames: [{ frameId: 0, url: 'https://chatgpt.com/checkout/openai_ie/cs_test' }],
|
||||
stateByFrame: {
|
||||
0: {
|
||||
hasPayPal: true,
|
||||
paypalCandidates: [{ tag: 'button', text: 'PayPal' }],
|
||||
billingFieldsVisible: true,
|
||||
hasSubscribeButton: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await executor.executePlusCheckoutBilling({});
|
||||
|
||||
assert.deepEqual(events.ensuredTabs[0], { source: 'plus-checkout', tabId: checkoutTab.id });
|
||||
assert.equal(events.messages.some((entry) => entry.message.type === 'PLUS_CHECKOUT_SELECT_PAYPAL' && entry.frameId === 0), true);
|
||||
assert.equal(events.messages.some((entry) => entry.message.type === 'PLUS_CHECKOUT_FILL_BILLING_ADDRESS' && entry.frameId === 0), true);
|
||||
assert.equal(events.messages.some((entry) => entry.message.type === 'PLUS_CHECKOUT_CLICK_SUBSCRIBE' && entry.frameId === 0), true);
|
||||
assert.equal(events.completed[0].step, 7);
|
||||
assert.equal(events.states.some((updates) => updates.plusCheckoutTabId === checkoutTab.id), true);
|
||||
assert.equal(events.logs.some((entry) => /当前已在 Plus Checkout 页面/.test(entry.message)), true);
|
||||
});
|
||||
|
||||
test('Plus checkout billing sends the billing command to the iframe that contains PayPal', async () => {
|
||||
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 },
|
||||
},
|
||||
});
|
||||
|
||||
await executor.executePlusCheckoutBilling({});
|
||||
|
||||
const selectMessage = events.messages.find((entry) => entry.message.type === 'PLUS_CHECKOUT_SELECT_PAYPAL');
|
||||
const fillMessage = events.messages.find((entry) => entry.message.type === 'PLUS_CHECKOUT_FILL_BILLING_ADDRESS');
|
||||
const subscribeMessage = events.messages.find((entry) => entry.message.type === 'PLUS_CHECKOUT_CLICK_SUBSCRIBE');
|
||||
assert.equal(selectMessage.frameId, 7);
|
||||
assert.equal(fillMessage.frameId, 8);
|
||||
assert.equal(subscribeMessage.frameId, 0);
|
||||
assert.equal(events.logs.some((entry) => /checkout iframe/.test(entry.message)), true);
|
||||
assert.equal(events.completed[0].step, 7);
|
||||
});
|
||||
|
||||
test('Plus checkout billing uses the autocomplete iframe for address suggestions when Stripe splits it out', async () => {
|
||||
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: [] },
|
||||
},
|
||||
});
|
||||
|
||||
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.frameId, 8);
|
||||
assert.equal(suggestionMessage.frameId, 9);
|
||||
assert.equal(ensureAddressMessage.frameId, 8);
|
||||
assert.equal(combinedFillMessage, undefined);
|
||||
assert.equal(events.logs.some((entry) => /Google 地址推荐/.test(entry.message)), true);
|
||||
assert.equal(events.completed[0].step, 7);
|
||||
});
|
||||
|
||||
test('Plus checkout billing reports when the payment iframe exists but cannot receive the content script', async () => {
|
||||
const { 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' },
|
||||
],
|
||||
stateByFrame: {
|
||||
0: { hasPayPal: false, paypalCandidates: [], hasSubscribeButton: true },
|
||||
},
|
||||
readyByFrame: {
|
||||
7: false,
|
||||
},
|
||||
});
|
||||
|
||||
await assert.rejects(
|
||||
executor.executePlusCheckoutBilling({}),
|
||||
/已定位到 PayPal 所在 iframe(frameId=7),但账单脚本无法注入该 iframe/
|
||||
);
|
||||
});
|
||||
+1
-1
@@ -493,7 +493,7 @@ Plus 模式可见步骤:
|
||||
|
||||
1. 第 1~5 步:沿用普通注册入口、邮箱、密码、注册验证码、资料填写链路。
|
||||
2. 第 6 步 `创建 Plus Checkout`:打开已登录 ChatGPT 页面,通过 `/api/auth/session` 读取 accessToken,再请求 `https://chatgpt.com/backend-api/payments/checkout` 创建 `chatgptplusplan` 的 checkout session,并打开 `https://chatgpt.com/checkout/openai_ie/{checkout_session_id}`。
|
||||
3. 第 7 步 `填写账单并提交订阅`:选择 PayPal,生成账单全名,从 `data/address-sources.js` 读取同国家 seed query,触发 checkout 内置 Google 地址推荐,选择推荐项并校验地址第 1 行、城市、州/省、邮编等结构化字段,再点击“订阅”。
|
||||
3. 第 7 步 `填写账单并提交订阅`:选择 PayPal,生成账单全名,从 `data/address-sources.js` 读取同国家 seed query,触发 checkout 内置 Google 地址推荐,选择推荐项并校验地址第 1 行、城市、州/省、邮编等结构化字段,再点击“订阅”。运行时会按 Stripe iframe 拆分执行:付款方式在 `elements-inner-payment` frame,账单地址在 `elements-inner-address` frame,Google 推荐在 `elements-inner-autocompl` frame 时单独点击推荐项。
|
||||
4. 第 8 步 `PayPal 登录与授权`:在 PayPal 页面填写 `paypalEmail / paypalPassword`,登录前固定等待 1 秒,关闭可见通行密钥提示,点击“同意并继续”。
|
||||
5. 第 9 步 `订阅回跳确认`:等待 PayPal 授权后回跳到 ChatGPT / OpenAI 页面,页面加载完成后固定等待 1 秒。
|
||||
6. 第 10 步:复用原 Step 7 OAuth 登录执行器,但状态和日志按 Plus 可见第 10 步记录。
|
||||
|
||||
@@ -175,6 +175,7 @@
|
||||
- `tests/background-tab-runtime-module.test.js`:测试标签运行时模块已接入且导出工厂,并覆盖等待标签完成时的 Stop 中断行为。
|
||||
- `tests/background-verification-flow-module.test.js`:测试验证码流程模块已接入且导出工厂。
|
||||
- `tests/phone-verification-flow.test.js`:测试手机号验证共享流程对 HeroSMS 的取号、复用、重发、换号与 Step 7 重开错误分支。
|
||||
- `tests/plus-checkout-billing-tab-resolution.test.js`:测试 Plus 第 7 步可直接接管当前 checkout 标签页,并把 PayPal、账单地址、Google 地址推荐和订阅按钮操作路由到对应 Stripe iframe。
|
||||
- `tests/cloudflare-temp-email-provider.test.js`:测试 Cloudflare Temp Email provider 的轮询与目标邮箱选择逻辑。
|
||||
- `tests/cloudflare-temp-email-utils.test.js`:测试 Cloudflare Temp Email 工具层的 URL、域名、邮件解析逻辑。
|
||||
- `tests/content-utils.test.js`:测试内容脚本公共工具层对 `mail.126.com` 等网页邮箱来源的识别逻辑。
|
||||
|
||||
Reference in New Issue
Block a user