Plus中添加GPC模式

This commit is contained in:
QLHazyCoder
2026-05-04 18:02:00 +08:00
parent 6160f4640d
commit b595f5366a
17 changed files with 2151 additions and 64 deletions
+298 -3
View File
@@ -9,6 +9,7 @@
const PLUS_CHECKOUT_PAYPAL_REDIRECT_TIMEOUT_MS = 20000;
const PLUS_PAYMENT_METHOD_PAYPAL = 'paypal';
const PLUS_PAYMENT_METHOD_GOPAY = 'gopay';
const PLUS_PAYMENT_METHOD_GPC_HELPER = 'gpc-helper';
const PAYMENT_METHOD_CONFIGS = {
[PLUS_PAYMENT_METHOD_PAYPAL]: {
id: PLUS_PAYMENT_METHOD_PAYPAL,
@@ -53,12 +54,14 @@
function createPlusCheckoutBillingExecutor(deps = {}) {
const {
addLog,
broadcastDataUpdate,
chrome,
completeStepFromBackground,
ensureContentScriptReadyOnTabUntilStopped,
fetch: fetchImpl = null,
generateRandomName,
getAddressSeedForCountry,
getState,
getTabId,
isTabAlive,
markCurrentRegistrationAccountUsed,
@@ -66,6 +69,7 @@
sleepWithStop,
waitForTabCompleteUntilStopped,
probeIpProxyExit = null,
throwIfStopped = () => {},
} = deps;
function isPlusCheckoutUrl(url = '') {
@@ -76,20 +80,307 @@
return String(value || '').replace(/\s+/g, ' ').trim();
}
function isGpcHelperCheckout(state = {}) {
return normalizePlusPaymentMethod(state?.plusPaymentMethod) === PLUS_PAYMENT_METHOD_GPC_HELPER
|| (normalizeText(state?.plusCheckoutSource) === PLUS_PAYMENT_METHOD_GPC_HELPER
&& Boolean(state?.gopayHelperReferenceId));
}
function compactCountryText(value = '') {
return normalizeText(value).toLowerCase().replace(/[^a-z0-9\u4e00-\u9fff]/g, '');
}
function normalizePlusPaymentMethod(value = '') {
return String(value || '').trim().toLowerCase() === PLUS_PAYMENT_METHOD_GOPAY
? PLUS_PAYMENT_METHOD_GOPAY
: PLUS_PAYMENT_METHOD_PAYPAL;
const rootScope = typeof self !== 'undefined' ? self : globalThis;
if (rootScope.GoPayUtils?.normalizePlusPaymentMethod) {
return rootScope.GoPayUtils.normalizePlusPaymentMethod(value);
}
const normalized = String(value || '').trim().toLowerCase();
if (normalized === PLUS_PAYMENT_METHOD_GPC_HELPER) {
return PLUS_PAYMENT_METHOD_GPC_HELPER;
}
return normalized === PLUS_PAYMENT_METHOD_GOPAY ? PLUS_PAYMENT_METHOD_GOPAY : PLUS_PAYMENT_METHOD_PAYPAL;
}
function getPaymentMethodConfig(method = PLUS_PAYMENT_METHOD_PAYPAL) {
return PAYMENT_METHOD_CONFIGS[normalizePlusPaymentMethod(method)] || PAYMENT_METHOD_CONFIGS[PLUS_PAYMENT_METHOD_PAYPAL];
}
function normalizeGpcHelperBaseUrl(apiUrl = '') {
const rootScope = typeof self !== 'undefined' ? self : globalThis;
if (rootScope.GoPayUtils?.normalizeGpcHelperBaseUrl) {
return rootScope.GoPayUtils.normalizeGpcHelperBaseUrl(apiUrl);
}
let normalized = String(apiUrl || '').trim().replace(/\/+$/g, '');
normalized = normalized.replace(/\/api\/checkout\/start$/i, '');
normalized = normalized.replace(/\/api\/gopay\/(?:otp|pin)$/i, '');
normalized = normalized.replace(/\/api\/card\/balance(?:\?.*)?$/i, '');
return normalized;
}
async function fetchJsonWithTimeout(url, options = {}, timeoutMs = 30000) {
const fetcher = typeof fetchImpl === 'function'
? fetchImpl
: (typeof fetch === 'function' ? fetch.bind(globalThis) : null);
if (typeof fetcher !== 'function') {
throw new Error('当前运行环境不支持 fetch,无法调用 GPC API。');
}
const controller = typeof AbortController === 'function' ? new AbortController() : null;
const timer = controller ? setTimeout(() => controller.abort(), Math.max(1000, Number(timeoutMs) || 30000)) : null;
try {
const response = await fetcher(url, { ...options, ...(controller ? { signal: controller.signal } : {}) });
const data = await response.json().catch(() => ({}));
return { response, data };
} finally {
if (timer) clearTimeout(timer);
}
}
function buildGpcOtpPayload(input = {}) {
const rootScope = typeof self !== 'undefined' ? self : globalThis;
if (rootScope.GoPayUtils?.buildGpcOtpPayload) {
return rootScope.GoPayUtils.buildGpcOtpPayload(input);
}
const payload = {
reference_id: String(input.reference_id ?? input.referenceId ?? '').trim(),
otp: String(input.otp ?? input.code ?? '').trim().replace(/[^\d]/g, ''),
card_key: String(input.card_key ?? input.cardKey ?? '').trim(),
};
const gopayGuid = String(input.gopay_guid ?? input.gopayGuid ?? '').trim();
const redirectUrl = String(input.redirect_url ?? input.redirectUrl ?? '').trim();
const flowId = String(input.flow_id ?? input.flowId ?? '').trim();
if (flowId) payload.flow_id = flowId;
if (gopayGuid) payload.gopay_guid = gopayGuid;
if (redirectUrl) payload.redirect_url = redirectUrl;
return payload;
}
function buildGpcOtpRetryPayload(input = {}) {
const rootScope = typeof self !== 'undefined' ? self : globalThis;
if (rootScope.GoPayUtils?.buildGpcOtpRetryPayload) {
return rootScope.GoPayUtils.buildGpcOtpRetryPayload(input);
}
const basePayload = buildGpcOtpPayload(input);
return { ...basePayload, code: basePayload.otp };
}
function buildGpcPinPayload(input = {}) {
const rootScope = typeof self !== 'undefined' ? self : globalThis;
if (rootScope.GoPayUtils?.buildGpcPinPayload) {
return rootScope.GoPayUtils.buildGpcPinPayload(input);
}
const payload = {
reference_id: String(input.reference_id ?? input.referenceId ?? '').trim(),
challenge_id: String(input.challenge_id ?? input.challengeId ?? '').trim(),
gopay_guid: String(input.gopay_guid ?? input.gopayGuid ?? '').trim(),
pin: String(input.pin ?? '').trim().replace(/[^\d]/g, ''),
card_key: String(input.card_key ?? input.cardKey ?? '').trim(),
};
const redirectUrl = String(input.redirect_url ?? input.redirectUrl ?? '').trim();
const flowId = String(input.flow_id ?? input.flowId ?? '').trim();
if (flowId) payload.flow_id = flowId;
if (redirectUrl) payload.redirect_url = redirectUrl;
return payload;
}
function buildGpcPinRetryPayload(input = {}) {
const rootScope = typeof self !== 'undefined' ? self : globalThis;
if (rootScope.GoPayUtils?.buildGpcPinRetryPayload) {
return rootScope.GoPayUtils.buildGpcPinRetryPayload(input);
}
const basePayload = buildGpcPinPayload(input);
return { ...basePayload, challengeId: basePayload.challenge_id };
}
function getGpcResponseErrorDetail(payload = {}, status = 0) {
const rootScope = typeof self !== 'undefined' ? self : globalThis;
if (rootScope.GoPayUtils?.extractGpcResponseErrorDetail) {
return rootScope.GoPayUtils.extractGpcResponseErrorDetail(payload, status);
}
if (payload && typeof payload === 'object') {
return payload.detail || payload.message || payload.error || payload.error_description || payload.reason || `HTTP ${status || 0}`;
}
return `HTTP ${status || 0}`;
}
async function postGpcJsonWithFallback(apiUrl, endpointPath, primaryPayload, fallbackPayload, timeoutMs = 30000) {
const requestUrl = `${apiUrl}${endpointPath}`;
const send = (payload) => fetchJsonWithTimeout(requestUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
}, timeoutMs);
const firstResponse = await send(primaryPayload);
if (firstResponse?.response?.ok || !fallbackPayload) {
return { ...firstResponse, retried: false, payload: primaryPayload };
}
const status = Number(firstResponse?.response?.status || 0);
if (status !== 400 && status !== 422) {
return { ...firstResponse, retried: false, payload: primaryPayload };
}
const firstDetail = getGpcResponseErrorDetail(firstResponse?.data, status);
await addLog(`步骤 7GPC 接口返回 ${status}${firstDetail}),使用兼容字段重试。`, 'warn');
const secondResponse = await send(fallbackPayload);
return {
...secondResponse,
retried: true,
payload: fallbackPayload,
firstError: firstDetail,
firstStatus: status,
};
}
function getStateInternal() {
if (typeof getState === 'function') {
return getState();
}
return Promise.resolve({});
}
async function requestGpcOtpInput({ title = '', message = '', referenceId = '' }) {
const requestId = `otp-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
const payload = {
plusManualConfirmationPending: true,
plusManualConfirmationRequestId: requestId,
plusManualConfirmationStep: 7,
plusManualConfirmationMethod: 'gopay-otp',
plusManualConfirmationTitle: title || 'GPC OTP 验证',
plusManualConfirmationMessage: message || '请输入 OTP 验证码',
gopayHelperOtpRequestId: requestId,
gopayHelperOtpReferenceId: referenceId,
gopayHelperResolvedOtp: '',
};
await setState(payload);
if (typeof broadcastDataUpdate === 'function') {
broadcastDataUpdate(payload);
}
return new Promise((resolve, reject) => {
const checkInterval = setInterval(async () => {
try {
throwIfStopped();
const currentState = await getStateInternal();
if (!currentState?.plusManualConfirmationPending || currentState?.plusManualConfirmationRequestId !== requestId) {
clearInterval(checkInterval);
const resolvedOtp = String(currentState?.gopayHelperResolvedOtp || '').trim().replace(/[^\d]/g, '');
if (resolvedOtp) {
resolve(resolvedOtp);
} else {
reject(new Error('OTP 输入已取消'));
}
}
} catch (error) {
clearInterval(checkInterval);
reject(error);
}
}, 500);
});
}
async function executeGpcHelperBilling(state = {}) {
const referenceId = String(state?.gopayHelperReferenceId || '').trim();
const apiUrl = normalizeGpcHelperBaseUrl(state?.gopayHelperApiUrl || '');
const cardKey = String(state?.gopayHelperCardKey || state?.gpcCardKey || state?.cardKey || '').trim();
if (!referenceId) {
throw new Error('步骤 7GPC 模式缺少 reference_id,请先执行步骤 6。');
}
if (!apiUrl) {
throw new Error('步骤 7GPC 模式缺少 API 地址。');
}
if (!cardKey) {
throw new Error('步骤 7GPC 模式缺少卡密。');
}
await addLog(`步骤 7:GPC 模式开始 OTP 验证(reference_id: ${referenceId}...`, 'info');
await addLog('步骤 7:等待用户输入 OTP...', 'info');
const otp = await requestGpcOtpInput({
title: 'GPC OTP 验证',
message: `请输入收到的 OTP 验证码(reference_id: ${referenceId}`,
referenceId,
});
const flowId = state?.gopayHelperFlowId
|| state?.gopayHelperStartPayload?.flow_id
|| state?.gopayHelperStartPayload?.flowId
|| state?.gopayHelperBalancePayload?.flow_id
|| state?.gopayHelperBalancePayload?.flowId
|| '';
const baseInput = {
reference_id: referenceId,
otp,
card_key: cardKey,
gopay_guid: state?.gopayHelperGoPayGuid || '',
redirect_url: state?.gopayHelperRedirectUrl || '',
flow_id: flowId,
};
await addLog('步骤 7:正在提交 OTP...', 'info');
const otpResponse = await postGpcJsonWithFallback(
apiUrl,
'/api/gopay/otp',
buildGpcOtpPayload(baseInput),
buildGpcOtpRetryPayload(baseInput),
30000
);
if (!otpResponse?.response?.ok) {
throw new Error(`步骤 7OTP 验证失败:${getGpcResponseErrorDetail(otpResponse?.data, otpResponse?.response?.status || 0)}`);
}
const otpData = otpResponse?.data || {};
const challengeId = String(
otpData?.challenge_id
|| otpData?.challengeId
|| state?.gopayHelperChallengeId
|| state?.gopayHelperStartPayload?.challenge_id
|| state?.gopayHelperStartPayload?.challengeId
|| ''
).trim();
const nextFlowId = String(otpData?.flow_id || otpData?.flowId || baseInput.flow_id || '').trim();
const gopayGuid = String(otpData?.gopay_guid || otpData?.gopayGuid || state?.gopayHelperGoPayGuid || '').trim();
const redirectUrl = String(otpData?.redirect_url || otpData?.redirectUrl || state?.gopayHelperRedirectUrl || '').trim();
if (!challengeId) {
throw new Error('步骤 7GPC OTP 验证后未返回 challenge_id。');
}
const pin = String(state?.gopayHelperPin || '').trim().replace(/[^\d]/g, '');
if (!pin) {
throw new Error('步骤 7GPC 模式缺少 PIN 配置。');
}
await setState({
gopayHelperChallengeId: challengeId,
gopayHelperFlowId: nextFlowId,
gopayHelperGoPayGuid: gopayGuid,
gopayHelperRedirectUrl: redirectUrl,
});
await addLog('步骤 7:正在提交 PIN...', 'info');
const pinInput = {
reference_id: referenceId,
challenge_id: challengeId,
gopay_guid: gopayGuid,
redirect_url: redirectUrl,
flow_id: nextFlowId,
pin,
card_key: cardKey,
};
const pinResponse = await postGpcJsonWithFallback(
apiUrl,
'/api/gopay/pin',
buildGpcPinPayload(pinInput),
buildGpcPinRetryPayload(pinInput),
30000
);
if (!pinResponse?.response?.ok) {
throw new Error(`步骤 7PIN 验证失败:${getGpcResponseErrorDetail(pinResponse?.data, pinResponse?.response?.status || 0)}`);
}
await setState({
plusCheckoutSource: PLUS_PAYMENT_METHOD_GPC_HELPER,
gopayHelperPinPayload: pinResponse?.data || null,
});
await addLog('步骤 7:GPC 支付完成,准备继续下一步。', 'ok');
await completeStepFromBackground(7, {
plusCheckoutSource: PLUS_PAYMENT_METHOD_GPC_HELPER,
});
}
function resolveMeiguodizhiCountryCode(value = '') {
const normalized = normalizeText(value);
const upper = normalized.toUpperCase();
@@ -617,6 +908,10 @@
}
async function executePlusCheckoutBilling(state = {}) {
if (isGpcHelperCheckout(state)) {
await executeGpcHelperBilling(state);
return;
}
const paymentMethod = normalizePlusPaymentMethod(state?.plusPaymentMethod);
const paymentConfig = getPaymentMethodConfig(paymentMethod);
const tabId = await getCheckoutTabId(state);