diff --git a/background.js b/background.js index 6bbac06..20b2788 100644 --- a/background.js +++ b/background.js @@ -62,11 +62,16 @@ const PLUS_GOPAY_STEP_DEFINITIONS = self.MultiPageStepDefinitions?.getSteps?.({ plusModeEnabled: true, plusPaymentMethod: 'gopay', }) || PLUS_PAYPAL_STEP_DEFINITIONS; +const PLUS_GPC_STEP_DEFINITIONS = self.MultiPageStepDefinitions?.getSteps?.({ + plusModeEnabled: true, + plusPaymentMethod: 'gpc-helper', +}) || PLUS_GOPAY_STEP_DEFINITIONS; const PLUS_STEP_DEFINITIONS = PLUS_PAYPAL_STEP_DEFINITIONS; const ALL_STEP_DEFINITIONS = self.MultiPageStepDefinitions?.getAllSteps?.() || [ ...NORMAL_STEP_DEFINITIONS, ...PLUS_PAYPAL_STEP_DEFINITIONS, ...PLUS_GOPAY_STEP_DEFINITIONS, + ...PLUS_GPC_STEP_DEFINITIONS, ]; const STEP_IDS = Array.from(new Set(ALL_STEP_DEFINITIONS .map((definition) => Number(definition?.id)) @@ -84,11 +89,16 @@ const PLUS_GOPAY_STEP_IDS = PLUS_GOPAY_STEP_DEFINITIONS .map((definition) => Number(definition?.id)) .filter(Number.isFinite) .sort((left, right) => left - right); +const PLUS_GPC_STEP_IDS = PLUS_GPC_STEP_DEFINITIONS + .map((definition) => Number(definition?.id)) + .filter(Number.isFinite) + .sort((left, right) => left - right); const PLUS_STEP_IDS = PLUS_PAYPAL_STEP_IDS; const LAST_STEP_ID = Math.max( NORMAL_STEP_IDS[NORMAL_STEP_IDS.length - 1] || 10, PLUS_PAYPAL_STEP_IDS[PLUS_PAYPAL_STEP_IDS.length - 1] || 10, - PLUS_GOPAY_STEP_IDS[PLUS_GOPAY_STEP_IDS.length - 1] || 10 + PLUS_GOPAY_STEP_IDS[PLUS_GOPAY_STEP_IDS.length - 1] || 10, + PLUS_GPC_STEP_IDS[PLUS_GPC_STEP_IDS.length - 1] || 10 ); const FINAL_OAUTH_CHAIN_START_STEP = 7; @@ -367,6 +377,7 @@ const HERO_SMS_SUPPORTED_COUNTRY_ID_SET = new Set(HERO_SMS_SUPPORTED_COUNTRY_IDS const FIVE_SIM_OPERATOR = DEFAULT_FIVE_SIM_OPERATOR; const PLUS_PAYMENT_METHOD_PAYPAL = 'paypal'; const PLUS_PAYMENT_METHOD_GOPAY = 'gopay'; +const PLUS_PAYMENT_METHOD_GPC_HELPER = 'gpc-helper'; const DEFAULT_PLUS_PAYMENT_METHOD = PLUS_PAYMENT_METHOD_PAYPAL; const DISPLAY_TIMEZONE = 'Asia/Shanghai'; const MICROSOFT_TOKEN_DNR_RULE_ID = 1001; @@ -407,7 +418,11 @@ function isPlusModeState(state = {}) { } function normalizePlusPaymentMethod(value = '') { - return String(value || '').trim().toLowerCase() === 'gopay' ? 'gopay' : 'paypal'; + 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 normalizeContributionModeSource(value = '') { @@ -460,9 +475,11 @@ function getStepDefinitionsForState(state = {}) { if (!isPlusModeState(state)) { return NORMAL_STEP_DEFINITIONS; } - return normalizePlusPaymentMethod(state?.plusPaymentMethod) === PLUS_PAYMENT_METHOD_GOPAY - ? PLUS_GOPAY_STEP_DEFINITIONS - : PLUS_PAYPAL_STEP_DEFINITIONS; + const paymentMethod = normalizePlusPaymentMethod(state?.plusPaymentMethod); + if (paymentMethod === PLUS_PAYMENT_METHOD_GPC_HELPER) { + return PLUS_GPC_STEP_DEFINITIONS; + } + return paymentMethod === PLUS_PAYMENT_METHOD_GOPAY ? PLUS_GOPAY_STEP_DEFINITIONS : PLUS_PAYPAL_STEP_DEFINITIONS; } function getStepIdsForState(state = {}) { @@ -476,9 +493,11 @@ function getStepIdsForState(state = {}) { if (!isPlusModeState(state)) { return NORMAL_STEP_IDS; } - return normalizePlusPaymentMethod(state?.plusPaymentMethod) === PLUS_PAYMENT_METHOD_GOPAY - ? PLUS_GOPAY_STEP_IDS - : PLUS_PAYPAL_STEP_IDS; + const paymentMethod = normalizePlusPaymentMethod(state?.plusPaymentMethod); + if (paymentMethod === PLUS_PAYMENT_METHOD_GPC_HELPER) { + return PLUS_GPC_STEP_IDS; + } + return paymentMethod === PLUS_PAYMENT_METHOD_GOPAY ? PLUS_GOPAY_STEP_IDS : PLUS_PAYPAL_STEP_IDS; } function getLastStepIdForState(state = {}) { @@ -579,6 +598,22 @@ const PERSISTED_SETTING_DEFAULTS = { gopayPhone: '', gopayOtp: '', gopayPin: '', + gopayHelperApiUrl: '', + gopayHelperCardKey: '', + gopayHelperPhoneNumber: '', + gopayHelperCountryCode: '+86', + gopayHelperPin: '', + gopayHelperReferenceId: '', + gopayHelperGoPayGuid: '', + gopayHelperRedirectUrl: '', + gopayHelperNextAction: '', + gopayHelperFlowId: '', + gopayHelperChallengeId: '', + gopayHelperStartPayload: null, + gopayHelperBalance: '', + gopayHelperBalancePayload: null, + gopayHelperBalanceUpdatedAt: 0, + gopayHelperBalanceError: '', autoRunSkipFailures: false, autoRunFallbackThreadIntervalMinutes: 0, oauthFlowTimeoutEnabled: true, @@ -714,6 +749,7 @@ const DEFAULT_STATE = { plusCheckoutUrl: null, // Plus checkout 运行时短链,不写入持久配置。 plusCheckoutCountry: 'DE', plusCheckoutCurrency: 'EUR', + plusCheckoutSource: '', plusBillingCountryText: '', plusBillingAddress: null, plusPaypalApprovedAt: null, @@ -725,6 +761,17 @@ const DEFAULT_STATE = { plusManualConfirmationMethod: '', plusManualConfirmationTitle: '', plusManualConfirmationMessage: '', + gopayHelperReferenceId: '', + gopayHelperGoPayGuid: '', + gopayHelperRedirectUrl: '', + gopayHelperNextAction: '', + gopayHelperFlowId: '', + gopayHelperChallengeId: '', + gopayHelperStartPayload: null, + gopayHelperPinPayload: null, + gopayHelperResolvedOtp: '', + gopayHelperOtpRequestId: '', + gopayHelperOtpReferenceId: '', flowStartTime: null, // 当前流程开始时间。 tabRegistry: {}, // 程序维护的标签页注册表。 sourceLastUrls: {}, // 各来源页面最近一次打开的地址记录。 @@ -1136,9 +1183,11 @@ function normalizePlusPaymentMethod(value = '') { if (rootScope.GoPayUtils?.normalizePlusPaymentMethod) { return rootScope.GoPayUtils.normalizePlusPaymentMethod(value); } - return String(value || '').trim().toLowerCase() === PLUS_PAYMENT_METHOD_GOPAY - ? PLUS_PAYMENT_METHOD_GOPAY - : PLUS_PAYMENT_METHOD_PAYPAL; + 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 normalizeFiveSimCountryId(value, fallback = FIVE_SIM_COUNTRY_ID) { @@ -2125,6 +2174,37 @@ function normalizePersistentSettingValue(key, value) { return self.GoPayUtils?.normalizeGoPayPin ? self.GoPayUtils.normalizeGoPayPin(value) : String(value || ''); + case 'gopayHelperPhoneNumber': + return self.GoPayUtils?.normalizeGoPayPhone + ? self.GoPayUtils.normalizeGoPayPhone(value) + : String(value || '').trim(); + case 'gopayHelperPin': + return self.GoPayUtils?.normalizeGoPayPin + ? self.GoPayUtils.normalizeGoPayPin(value) + : String(value || ''); + case 'gopayHelperCountryCode': + return self.GoPayUtils?.normalizeGoPayCountryCode + ? self.GoPayUtils.normalizeGoPayCountryCode(value) + : String(value || '+86').trim(); + case 'gopayHelperApiUrl': + return self.GoPayUtils?.normalizeGpcHelperBaseUrl + ? self.GoPayUtils.normalizeGpcHelperBaseUrl(value) + : String(value || '').trim().replace(/\/+$/g, ''); + case 'gopayHelperCardKey': + case 'gopayHelperReferenceId': + case 'gopayHelperGoPayGuid': + case 'gopayHelperRedirectUrl': + case 'gopayHelperNextAction': + case 'gopayHelperFlowId': + case 'gopayHelperChallengeId': + case 'gopayHelperBalance': + case 'gopayHelperBalanceError': + return String(value || '').trim(); + case 'gopayHelperBalancePayload': + case 'gopayHelperStartPayload': + return value && typeof value === 'object' && !Array.isArray(value) ? value : null; + case 'gopayHelperBalanceUpdatedAt': + return Math.max(0, Number(value) || 0); case 'autoRunSkipFailures': case 'oauthFlowTimeoutEnabled': case 'autoRunDelayEnabled': @@ -7143,6 +7223,7 @@ function getDownstreamStateResets(step, state = {}) { plusCheckoutUrl: null, plusCheckoutCountry: 'DE', plusCheckoutCurrency: 'EUR', + plusCheckoutSource: '', plusBillingCountryText: '', plusBillingAddress: null, plusPaypalApprovedAt: null, @@ -7154,6 +7235,17 @@ function getDownstreamStateResets(step, state = {}) { plusManualConfirmationMethod: '', plusManualConfirmationTitle: '', plusManualConfirmationMessage: '', + gopayHelperReferenceId: '', + gopayHelperGoPayGuid: '', + gopayHelperRedirectUrl: '', + gopayHelperNextAction: '', + gopayHelperFlowId: '', + gopayHelperChallengeId: '', + gopayHelperStartPayload: null, + gopayHelperPinPayload: null, + gopayHelperResolvedOtp: '', + gopayHelperOtpRequestId: '', + gopayHelperOtpReferenceId: '', }; if (step <= 1) { @@ -7237,6 +7329,9 @@ function getDownstreamStateResets(step, state = {}) { plusManualConfirmationMethod: '', plusManualConfirmationTitle: '', plusManualConfirmationMessage: '', + gopayHelperResolvedOtp: '', + gopayHelperOtpRequestId: '', + gopayHelperOtpReferenceId: '', } : {}), ...(step === 8 ? { plusPaypalApprovedAt: null, @@ -9188,6 +9283,100 @@ async function maybeSwitchIpProxyAfterAutoRunRoundSuccess(payload = {}) { return switchResult; } +function resolveGpcHelperBaseUrl(apiUrl = '') { + if (self.GoPayUtils?.normalizeGpcHelperBaseUrl) { + return self.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; +} + +function buildGpcCardBalanceRequestUrl(apiUrl = '', cardKey = '') { + if (self.GoPayUtils?.buildGpcCardBalanceUrl) { + return self.GoPayUtils.buildGpcCardBalanceUrl(apiUrl, cardKey); + } + const baseUrl = resolveGpcHelperBaseUrl(apiUrl); + if (!baseUrl) { + return ''; + } + return `${baseUrl}/api/card/balance?card_key=${encodeURIComponent(String(cardKey || '').trim())}`; +} + +function formatGpcCardBalancePayload(payload = {}) { + if (self.GoPayUtils?.formatGpcBalancePayload) { + return self.GoPayUtils.formatGpcBalancePayload(payload); + } + if (!payload || typeof payload !== 'object') { + return ''; + } + const remaining = payload.remaining_uses ?? payload.remainingUses ?? payload.balance ?? payload.remaining; + const status = String(payload.card_status || payload.cardStatus || payload.status || '').trim(); + return [ + remaining !== undefined && remaining !== null && String(remaining).trim() !== '' ? `余额 ${remaining}` : '', + status ? `状态 ${status}` : '', + ].filter(Boolean).join(','); +} + +async function refreshGpcCardBalance(state = {}, options = {}) { + const apiUrl = String(state?.gopayHelperApiUrl || '').trim(); + const cardKey = String(state?.gopayHelperCardKey || state?.gpcCardKey || state?.cardKey || '').trim(); + if (!apiUrl) { + throw new Error('缺少 GPC API 地址。'); + } + if (!cardKey) { + throw new Error('缺少 GPC 卡密。'); + } + const requestUrl = buildGpcCardBalanceRequestUrl(apiUrl, cardKey); + if (!requestUrl) { + throw new Error('缺少 GPC API 地址。'); + } + + const response = await fetch(requestUrl, { + method: 'GET', + headers: { Accept: 'application/json' }, + }); + const rawText = await response.text(); + let payload = {}; + try { + payload = rawText ? JSON.parse(rawText) : {}; + } catch { + payload = { raw: rawText }; + } + const balanceText = formatGpcCardBalancePayload(payload) || rawText || '未知'; + const updates = { + gopayHelperBalance: balanceText, + gopayHelperBalancePayload: payload && typeof payload === 'object' && !Array.isArray(payload) ? payload : { raw: String(payload || '') }, + gopayHelperBalanceUpdatedAt: Date.now(), + gopayHelperBalanceError: '', + }; + const flowId = String(payload?.flow_id || payload?.flowId || '').trim(); + if (flowId) { + updates.gopayHelperFlowId = flowId; + } + + if (!response.ok || payload?.ok === false) { + const detail = payload?.error || payload?.message || payload?.detail || `HTTP ${response.status}`; + const errorUpdates = { ...updates, gopayHelperBalanceError: String(detail || '余额查询失败') }; + await setPersistentSettings(errorUpdates); + broadcastDataUpdate(errorUpdates); + throw new Error(String(detail || '余额查询失败')); + } + + await setPersistentSettings(updates); + broadcastDataUpdate(updates); + const reason = String(options?.reason || '').trim(); + await addLog( + reason === 'round_success' + ? `GPC 余额已更新:${balanceText}` + : `GPC 余额查询成功:${balanceText}`, + 'info' + ); + return { balance: balanceText, payload, updatedAt: updates.gopayHelperBalanceUpdatedAt }; +} + const autoRunController = self.MultiPageBackgroundAutoRunController?.createAutoRunController({ addLog, appendAccountRunRecord: (...args) => appendAndBroadcastAccountRunRecord(...args), @@ -10109,6 +10298,8 @@ const plusCheckoutCreateExecutor = self.MultiPageBackgroundPlusCheckoutCreate?.c chrome, completeStepFromBackground, ensureContentScriptReadyOnTabUntilStopped, + fetch: typeof fetch === 'function' ? fetch.bind(globalThis) : null, + markCurrentRegistrationAccountUsed, registerTab, sendTabMessageUntilStopped, setState, @@ -10118,18 +10309,21 @@ const plusCheckoutCreateExecutor = self.MultiPageBackgroundPlusCheckoutCreate?.c }); const plusCheckoutBillingExecutor = self.MultiPageBackgroundPlusCheckoutBilling?.createPlusCheckoutBillingExecutor({ addLog, + broadcastDataUpdate, chrome, completeStepFromBackground, ensureContentScriptReadyOnTabUntilStopped, fetch: typeof fetch === 'function' ? fetch.bind(globalThis) : null, generateRandomName, getAddressSeedForCountry: self.MultiPageAddressSources?.getAddressSeedForCountry, + getState, getTabId, isTabAlive, markCurrentRegistrationAccountUsed, sendTabMessageUntilStopped, setState, sleepWithStop, + throwIfStopped, waitForTabCompleteUntilStopped, waitForTabUrlMatchUntilStopped, probeIpProxyExit, @@ -10253,6 +10447,7 @@ const messageRouter = self.MultiPageBackgroundMessageRouter?.createMessageRouter executeStepViaCompletionSignal, exportSettingsBundle, fetchGeneratedEmail, + refreshGpcCardBalance, finalizePhoneActivationAfterSuccessfulFlow, finalizeStep3Completion: async () => { const currentState = await getState(); @@ -10360,14 +10555,17 @@ function buildStepRegistry(definitions = []) { const normalStepRegistry = buildStepRegistry(NORMAL_STEP_DEFINITIONS); const plusPayPalStepRegistry = buildStepRegistry(PLUS_PAYPAL_STEP_DEFINITIONS); const plusGoPayStepRegistry = buildStepRegistry(PLUS_GOPAY_STEP_DEFINITIONS); +const plusGpcStepRegistry = buildStepRegistry(PLUS_GPC_STEP_DEFINITIONS); function getStepRegistryForState(state = {}) { if (!isPlusModeState(state)) { return normalStepRegistry; } - return normalizePlusPaymentMethod(state?.plusPaymentMethod) === PLUS_PAYMENT_METHOD_GOPAY - ? plusGoPayStepRegistry - : plusPayPalStepRegistry; + const paymentMethod = normalizePlusPaymentMethod(state?.plusPaymentMethod); + if (paymentMethod === PLUS_PAYMENT_METHOD_GPC_HELPER) { + return plusGpcStepRegistry; + } + return paymentMethod === PLUS_PAYMENT_METHOD_GOPAY ? plusGoPayStepRegistry : plusPayPalStepRegistry; } async function requestOAuthUrlFromPanel(state, options = {}) { diff --git a/background/message-router.js b/background/message-router.js index a224656..5a32d02 100644 --- a/background/message-router.js +++ b/background/message-router.js @@ -33,6 +33,7 @@ executeStepViaCompletionSignal, exportSettingsBundle, fetchGeneratedEmail, + refreshGpcCardBalance, finalizePhoneActivationAfterSuccessfulFlow, finalizeStep3Completion, finalizeIcloudAliasAfterSuccessfulFlow, @@ -203,6 +204,22 @@ }) || null; } + function normalizePlusPaymentMethodForDisplay(value = '') { + const normalized = String(value || '').trim().toLowerCase(); + if (normalized === 'gpc-helper') { + return 'gpc-helper'; + } + return normalized === 'gopay' ? 'gopay' : 'paypal'; + } + + function getPlusPaymentMethodLabel(value = '') { + const method = normalizePlusPaymentMethodForDisplay(value); + if (method === 'gpc-helper') { + return 'GPC'; + } + return method === 'gopay' ? 'GoPay' : 'PayPal'; + } + async function handlePlatformVerifyStepData(payload) { if (payload.localhostUrl) { await closeLocalhostCallbackTabs(payload.localhostUrl); @@ -550,6 +567,8 @@ const confirmed = Boolean(message.payload?.confirmed); const requestId = String(message.payload?.requestId || '').trim(); const currentRequestId = String(currentState?.plusManualConfirmationRequestId || '').trim(); + const method = String(currentState?.plusManualConfirmationMethod || '').trim().toLowerCase(); + const isGpcOtp = method === 'gopay-otp'; if (!currentState?.plusManualConfirmationPending) { return { ok: true, ignored: true }; } @@ -565,15 +584,31 @@ plusManualConfirmationTitle: '', plusManualConfirmationMessage: '', }; + + if (isGpcOtp && confirmed) { + const otp = String(message.payload?.otp || message.payload?.code || '').trim().replace(/[^\d]/g, ''); + if (!otp) { + throw new Error('请输入 GPC OTP 验证码。'); + } + const otpUpdates = { + ...clearManualConfirmationState, + gopayHelperResolvedOtp: otp, + }; + await setState(otpUpdates); + if (typeof broadcastDataUpdate === 'function') { + broadcastDataUpdate(otpUpdates); + } + await addLog(`步骤 ${step}:已收到 GPC OTP,准备提交验证。`, 'ok'); + return { ok: true }; + } + await setState(clearManualConfirmationState); if (typeof broadcastDataUpdate === 'function') { broadcastDataUpdate(clearManualConfirmationState); } if (confirmed) { - const methodLabel = String(currentState?.plusManualConfirmationMethod || '').trim().toLowerCase() === 'gopay' - ? 'GoPay' - : '手动'; + const methodLabel = method === 'gopay' ? 'GoPay' : '手动'; await addLog(`步骤 ${step}:已确认${methodLabel}订阅完成,准备继续下一步。`, 'ok'); await completeStepFromBackground(step, { plusManualConfirmationMethod: currentState?.plusManualConfirmationMethod || '', @@ -582,9 +617,9 @@ return { ok: true }; } - const cancelMessage = String(currentState?.plusManualConfirmationMethod || '').trim().toLowerCase() === 'gopay' + const cancelMessage = method === 'gopay' ? '已取消 GoPay 订阅确认' - : '已取消当前手动确认'; + : (isGpcOtp ? '已取消 GPC OTP 输入' : '已取消当前手动确认'); await setStepStatus(step, 'failed'); await addLog(`步骤 ${step}:${cancelMessage}。`, 'warn'); await appendManualAccountRunRecordIfNeeded(`step${step}_failed`, null, cancelMessage); @@ -836,8 +871,8 @@ const modeChanged = Object.prototype.hasOwnProperty.call(updates, 'plusModeEnabled') && Boolean(currentState?.plusModeEnabled) !== Boolean(updates.plusModeEnabled); const plusPaymentChanged = Object.prototype.hasOwnProperty.call(updates, 'plusPaymentMethod') - && String(currentState?.plusPaymentMethod || 'paypal').trim().toLowerCase() - !== String(updates.plusPaymentMethod || 'paypal').trim().toLowerCase(); + && normalizePlusPaymentMethodForDisplay(currentState?.plusPaymentMethod || 'paypal') + !== normalizePlusPaymentMethodForDisplay(updates.plusPaymentMethod || 'paypal'); const nextPlusModeEnabled = Object.prototype.hasOwnProperty.call(updates, 'plusModeEnabled') ? Boolean(updates.plusModeEnabled) : Boolean(currentState?.plusModeEnabled); @@ -912,11 +947,9 @@ await setContributionMode(true); } if (modeChanged) { - const selectedPlusPaymentMethod = String( - (stateUpdates.plusPaymentMethod ?? currentState?.plusPaymentMethod ?? 'paypal') - ).trim().toLowerCase() === 'gopay' - ? 'GoPay' - : 'PayPal'; + const selectedPlusPaymentMethod = getPlusPaymentMethodLabel( + stateUpdates.plusPaymentMethod ?? currentState?.plusPaymentMethod ?? 'paypal' + ); await addLog( Boolean(updates.plusModeEnabled) ? `Plus 模式已开启,已切换为 Plus Checkout 步骤,当前支付方式:${selectedPlusPaymentMethod}。` @@ -924,16 +957,28 @@ 'info' ); } else if (plusPaymentChanged && nextPlusModeEnabled) { - const selectedPlusPaymentMethod = String( + const selectedPlusPaymentMethod = getPlusPaymentMethodLabel( stateUpdates.plusPaymentMethod ?? currentState?.plusPaymentMethod ?? 'paypal' - ).trim().toLowerCase() === 'gopay' - ? 'GoPay' - : 'PayPal'; + ); await addLog(`Plus 支付方式已切换为 ${selectedPlusPaymentMethod},已更新对应的 Plus 步骤。`, 'info'); } return { ok: true, state: await getState(), proxyRouting }; } + case 'REFRESH_GPC_CARD_BALANCE': { + if (typeof refreshGpcCardBalance !== 'function') { + throw new Error('GPC 卡密余额查询能力尚未接入。'); + } + const state = await getState(); + const result = await refreshGpcCardBalance({ + ...(state || {}), + ...(message.payload || {}), + }, { + reason: message.payload?.reason, + }); + return { ok: true, ...result }; + } + case 'RUN_IP_PROXY_AUTO_SYNC_NOW': { if (typeof runIpProxyAutoSync !== 'function') { throw new Error('IP 代理自动同步能力尚未接入。'); diff --git a/background/steps/create-plus-checkout.js b/background/steps/create-plus-checkout.js index b4538fd..e108a16 100644 --- a/background/steps/create-plus-checkout.js +++ b/background/steps/create-plus-checkout.js @@ -4,7 +4,9 @@ const PLUS_CHECKOUT_SOURCE = 'plus-checkout'; const PLUS_CHECKOUT_ENTRY_URL = 'https://chatgpt.com/'; const PLUS_CHECKOUT_INJECT_FILES = ['content/utils.js', 'content/plus-checkout.js']; + const PLUS_PAYMENT_METHOD_PAYPAL = 'paypal'; const PLUS_PAYMENT_METHOD_GOPAY = 'gopay'; + const PLUS_PAYMENT_METHOD_GPC_HELPER = 'gpc-helper'; function createPlusCheckoutCreateExecutor(deps = {}) { const { @@ -12,21 +14,42 @@ chrome, completeStepFromBackground, ensureContentScriptReadyOnTabUntilStopped, + fetch: fetchImpl = null, + markCurrentRegistrationAccountUsed = null, registerTab, sendTabMessageUntilStopped, setState, sleepWithStop, waitForTabCompleteUntilStopped, + throwIfStopped = () => {}, } = deps; function normalizePlusPaymentMethod(value = '') { - return String(value || '').trim().toLowerCase() === 'gopay' ? 'gopay' : '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 getCheckoutModeLabel(state = {}) { - return normalizePlusPaymentMethod(state?.plusPaymentMethod) === 'gopay' - ? 'GoPay 订阅页' - : 'Plus Checkout'; + const paymentMethod = normalizePlusPaymentMethod(state?.plusPaymentMethod); + if (paymentMethod === PLUS_PAYMENT_METHOD_GPC_HELPER) { + return 'GPC 订阅页'; + } + return paymentMethod === PLUS_PAYMENT_METHOD_GOPAY ? 'GoPay 订阅页' : 'Plus Checkout'; + } + + function getPlusPaymentMethodLabel(method = PLUS_PAYMENT_METHOD_PAYPAL) { + const paymentMethod = normalizePlusPaymentMethod(method); + if (paymentMethod === PLUS_PAYMENT_METHOD_GPC_HELPER) { + return 'GPC'; + } + return paymentMethod === PLUS_PAYMENT_METHOD_GOPAY ? 'GoPay' : 'PayPal'; } async function openFreshChatGptTabForCheckoutCreate() { @@ -41,19 +64,345 @@ return tabId; } - function normalizePlusPaymentMethod(value = '') { - return String(value || '').trim().toLowerCase() === PLUS_PAYMENT_METHOD_GOPAY ? 'gopay' : 'paypal'; + function normalizeHelperCountryCode(countryCode = '86') { + const digits = String(countryCode || '').replace(/\D/g, ''); + return digits || '86'; } - function getPlusPaymentMethodLabel(method = 'paypal') { - return normalizePlusPaymentMethod(method) === PLUS_PAYMENT_METHOD_GOPAY ? 'GoPay' : 'PayPal'; + function normalizeHelperPhoneNumber(phone = '', countryCode = '86') { + const cleaned = String(phone || '').replace(/\D/g, ''); + const countryDigits = normalizeHelperCountryCode(countryCode); + if (countryDigits && cleaned.startsWith(countryDigits) && cleaned.length > countryDigits.length) { + return cleaned.slice(countryDigits.length); + } + return cleaned; + } + + function resolveGpcHelperCardKey(state = {}) { + const cardKey = String(state?.gopayHelperCardKey || state?.gpcCardKey || state?.cardKey || '').trim(); + if (!cardKey) { + throw new Error('创建 GPC 订单失败:缺少卡密。'); + } + return cardKey; + } + + function resolveGpcHelperCustomerEmail(state = {}) { + const email = String( + state?.email + || state?.currentEmail + || state?.registrationEmail + || state?.accountEmail + || state?.mailboxEmail + || '' + ).trim().toLowerCase(); + if (!email) { + throw new Error('创建 GPC 订单失败:缺少当前轮邮箱。'); + } + return email; + } + + function parseGpcAmount(value) { + if (typeof value === 'number') { + return Number.isFinite(value) ? { amount: value, raw: String(value) } : null; + } + if (typeof value !== 'string') { + return null; + } + const raw = String(value || '').trim(); + if (!raw || !/\d/.test(raw)) { + return null; + } + const match = raw.match(/([+-]?\d{1,3}(?:[.,]\d{3})*(?:[.,]\d{1,2})|[+-]?\d+(?:[.,]\d{1,2})?)/); + if (!match) { + return null; + } + let numericText = String(match[1] || '').trim(); + const lastComma = numericText.lastIndexOf(','); + const lastDot = numericText.lastIndexOf('.'); + if (lastComma > -1 && lastDot > -1) { + const decimalSeparator = lastComma > lastDot ? ',' : '.'; + const thousandsSeparator = decimalSeparator === ',' ? '.' : ','; + numericText = numericText + .replace(new RegExp(`\\${thousandsSeparator}`, 'g'), '') + .replace(decimalSeparator, '.'); + } else if (lastComma > -1) { + numericText = numericText.replace(',', '.'); + } + const amount = Number(numericText.replace(/[^\d.+-]/g, '')); + return Number.isFinite(amount) ? { amount, raw } : null; + } + + function isGpcAmountKey(key = '') { + const normalized = String(key || '').trim().toLowerCase().replace(/[^a-z0-9]+/g, '_'); + if (!normalized) { + return false; + } + if (/(?:^|_)(?:id|guid|uuid|phone|country|postal|zip|code|count|status|time|timestamp|created|updated|expires|challenge|client|reference|currency|state)(?:_|$)/i.test(normalized)) { + return false; + } + return /(?:amount|balance|total|due|payable|gross|subtotal|price|charge)/i.test(normalized); + } + + function findGpcNonZeroAmount(payload = {}) { + const seen = new Set(); + function visit(value, path = [], depth = 0) { + if (value == null || depth > 10) { + return null; + } + const key = path[path.length - 1] || ''; + if (isGpcAmountKey(key)) { + const parsed = parseGpcAmount(value); + if (parsed && Math.abs(parsed.amount) >= 0.005) { + return { ...parsed, path: path.join('.') }; + } + } + if (typeof value !== 'object') { + return null; + } + if (seen.has(value)) { + return null; + } + seen.add(value); + if (Array.isArray(value)) { + for (let index = 0; index < value.length; index += 1) { + const found = visit(value[index], [...path, String(index)], depth + 1); + if (found) return found; + } + return null; + } + for (const [childKey, childValue] of Object.entries(value)) { + const found = visit(childValue, [...path, childKey], depth + 1); + if (found) return found; + } + return null; + } + return visit(payload); + } + + async function abortGpcNonFreeTrialIfNeeded(data = {}, state = {}) { + const nonZeroAmount = findGpcNonZeroAmount(data); + if (!nonZeroAmount) { + return; + } + const amountLabel = nonZeroAmount.raw || String(nonZeroAmount.amount); + await addLog(`步骤 6:GPC 接口返回余额非 0(${amountLabel}),当前账号没有免费试用资格,将跳过当前账号。`, 'warn'); + if (typeof markCurrentRegistrationAccountUsed === 'function') { + await markCurrentRegistrationAccountUsed(state, { + reason: 'plus-checkout-non-free-trial', + logPrefix: 'GPC:当前账号没有免费试用资格', + }); + } + throw new Error(`PLUS_CHECKOUT_NON_FREE_TRIAL::步骤 6:GPC 接口返回余额非 0(${amountLabel}),当前账号没有免费试用资格,已跳过支付提交。`); + } + + 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; + } + + function buildGpcHelperApiUrl(apiUrl = '', path = '') { + const rootScope = typeof self !== 'undefined' ? self : globalThis; + if (rootScope.GoPayUtils?.buildGpcHelperApiUrl) { + return rootScope.GoPayUtils.buildGpcHelperApiUrl(apiUrl, path); + } + const baseUrl = normalizeGpcHelperBaseUrl(apiUrl); + if (!baseUrl) { + return ''; + } + const normalizedPath = String(path || '').startsWith('/') ? String(path || '') : `/${String(path || '')}`; + return `${baseUrl}${normalizedPath}`; + } + + 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); + } + } + + async function readAccessTokenFromChatGptSessionTab(tabId) { + await waitForTabCompleteUntilStopped(tabId); + await sleepWithStop(1000); + await ensureContentScriptReadyOnTabUntilStopped(PLUS_CHECKOUT_SOURCE, tabId, { + inject: PLUS_CHECKOUT_INJECT_FILES, + injectSource: PLUS_CHECKOUT_SOURCE, + logMessage: '步骤 6:正在等待 ChatGPT 页面完成加载,再继续获取 accessToken...', + }); + + const sessionResult = await sendTabMessageUntilStopped(tabId, PLUS_CHECKOUT_SOURCE, { + type: 'PLUS_CHECKOUT_GET_STATE', + source: 'background', + payload: { + includeSession: true, + includeAccessToken: true, + }, + }); + if (sessionResult?.error) { + throw new Error(sessionResult.error); + } + return String(sessionResult?.accessToken || sessionResult?.session?.accessToken || '').trim(); + } + + async function generateGpcCheckoutFromApi(accessToken = '', state = {}) { + const token = String(accessToken || '').trim(); + if (!token) { + throw new Error('创建 GPC 订单失败:缺少 accessToken。'); + } + const apiUrl = buildGpcHelperApiUrl(state?.gopayHelperApiUrl, '/api/checkout/start'); + if (!apiUrl) { + throw new Error('创建 GPC 订单失败:缺少 API 地址。'); + } + const phoneNumber = String(state?.gopayHelperPhoneNumber || '').trim(); + const countryCode = normalizeHelperCountryCode(state?.gopayHelperCountryCode || '86'); + const pin = String(state?.gopayHelperPin || '').trim(); + const cardKey = resolveGpcHelperCardKey(state); + if (!phoneNumber) { + throw new Error('创建 GPC 订单失败:缺少手机号。'); + } + if (!pin) { + throw new Error('创建 GPC 订单失败:缺少 PIN。'); + } + + throwIfStopped(); + const payload = { + token, + entry_point: 'all_plans_pricing_modal', + plan_name: 'chatgptplusplan', + billing_details: { country: 'ID', currency: 'IDR' }, + promo_campaign: { + promo_campaign_id: 'plus-1-month-free', + is_coupon_from_query_param: false, + }, + checkout_ui_mode: 'custom', + proxy: { type: 'direct', url: '' }, + tax_region: { + country: 'US', + line1: '1208 Oakdale Street', + city: 'Jonesboro', + postal_code: '72401', + state: 'AR', + }, + customer_email: resolveGpcHelperCustomerEmail(state), + card_key: cardKey, + gopay_link: { + type: 'gopay', + country_code: countryCode, + phone_number: normalizeHelperPhoneNumber(phoneNumber, countryCode), + }, + }; + + const { response, data } = await fetchJsonWithTimeout(apiUrl, { + method: 'POST', + headers: { + Accept: '*/*', + 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8', + 'Content-Type': 'application/json', + }, + body: JSON.stringify(payload), + }, 30000); + + const referenceId = String(data?.reference_id || data?.referenceId || '').trim(); + const gopayGuid = String(data?.gopay_guid || data?.gopayGuid || '').trim(); + const redirectUrl = String(data?.redirect_url || data?.redirectUrl || '').trim(); + const nextAction = String(data?.next_action || data?.nextAction || '').trim(); + const flowId = String(data?.flow_id || data?.flowId || '').trim(); + const challengeId = String(data?.challenge_id || data?.challengeId || '').trim(); + + if (response?.ok) { + await abortGpcNonFreeTrialIfNeeded(data, state); + } + + if (!response?.ok || !referenceId) { + const rootScope = typeof self !== 'undefined' ? self : globalThis; + const detail = rootScope.GoPayUtils?.extractGpcResponseErrorDetail + ? rootScope.GoPayUtils.extractGpcResponseErrorDetail(data, response?.status || 0) + : (data?.detail || data?.message || data?.error || `HTTP ${response?.status || 0}`); + throw new Error(`创建 GPC 订单失败:${detail}`); + } + + return { + referenceId, + gopayGuid, + redirectUrl, + nextAction, + flowId, + challengeId, + responsePayload: data && typeof data === 'object' && !Array.isArray(data) ? data : null, + country: 'ID', + currency: 'IDR', + checkoutSource: PLUS_PAYMENT_METHOD_GPC_HELPER, + }; + } + + async function executeGpcCheckoutCreate(state = {}) { + let accessToken = String(state?.contributionAccessToken || state?.accessToken || state?.chatgptAccessToken || '').trim(); + if (!accessToken) { + await addLog('步骤 6:正在获取 accessToken...', 'info'); + const tokenTabId = await openFreshChatGptTabForCheckoutCreate(); + try { + accessToken = await readAccessTokenFromChatGptSessionTab(tokenTabId); + } finally { + if (chrome?.tabs?.remove && Number.isInteger(tokenTabId)) { + await chrome.tabs.remove(tokenTabId).catch(() => {}); + } + } + } + if (!accessToken) { + throw new Error('步骤 6:GPC 模式获取 accessToken 失败。'); + } + + await addLog('步骤 6:正在调用 GPC 接口创建订单...', 'info'); + const result = await generateGpcCheckoutFromApi(accessToken, state); + await setState({ + plusCheckoutTabId: null, + plusCheckoutUrl: '', + plusCheckoutCountry: result.country || 'ID', + plusCheckoutCurrency: result.currency || 'IDR', + plusCheckoutSource: result.checkoutSource, + gopayHelperReferenceId: result.referenceId, + gopayHelperGoPayGuid: result.gopayGuid, + gopayHelperRedirectUrl: result.redirectUrl, + gopayHelperNextAction: result.nextAction, + gopayHelperFlowId: result.flowId, + gopayHelperChallengeId: result.challengeId, + gopayHelperStartPayload: result.responsePayload, + }); + await addLog('步骤 6:GPC 订单已创建,准备继续下一步。', 'info'); + await completeStepFromBackground(6, { + plusCheckoutCountry: result.country || 'ID', + plusCheckoutCurrency: result.currency || 'IDR', + plusCheckoutSource: result.checkoutSource, + }); } async function executePlusCheckoutCreate(state = {}) { const paymentMethod = normalizePlusPaymentMethod(state?.plusPaymentMethod); + if (paymentMethod === PLUS_PAYMENT_METHOD_GPC_HELPER) { + await executeGpcCheckoutCreate(state); + return; + } + const paymentMethodLabel = getPlusPaymentMethodLabel(paymentMethod); const checkoutModeLabel = getCheckoutModeLabel(state); - await addLog('步骤 6:正在新打开 ChatGPT 会话页,准备创建 Plus Checkout...', 'info'); + await addLog(`步骤 6:正在打开新的 ChatGPT 会话,准备创建${checkoutModeLabel}...`, 'info'); const tabId = await openFreshChatGptTabForCheckoutCreate(); await waitForTabCompleteUntilStopped(tabId); @@ -92,6 +441,7 @@ plusCheckoutUrl: result.checkoutUrl, plusCheckoutCountry: result.country || 'DE', plusCheckoutCurrency: result.currency || 'EUR', + plusCheckoutSource: '', }); await addLog(`步骤 6:Plus Checkout 页面已就绪(${paymentMethodLabel} / ${result.country || 'DE'} ${result.currency || 'EUR'}),准备继续下一步。`, 'info'); diff --git a/background/steps/fill-plus-checkout.js b/background/steps/fill-plus-checkout.js index b3126c5..62f4778 100644 --- a/background/steps/fill-plus-checkout.js +++ b/background/steps/fill-plus-checkout.js @@ -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(`步骤 7:GPC 接口返回 ${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('步骤 7:GPC 模式缺少 reference_id,请先执行步骤 6。'); + } + if (!apiUrl) { + throw new Error('步骤 7:GPC 模式缺少 API 地址。'); + } + if (!cardKey) { + throw new Error('步骤 7:GPC 模式缺少卡密。'); + } + 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(`步骤 7:OTP 验证失败:${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('步骤 7:GPC OTP 验证后未返回 challenge_id。'); + } + const pin = String(state?.gopayHelperPin || '').trim().replace(/[^\d]/g, ''); + if (!pin) { + throw new Error('步骤 7:GPC 模式缺少 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(`步骤 7:PIN 验证失败:${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); diff --git a/content/plus-checkout.js b/content/plus-checkout.js index bba15b6..8a5759a 100644 --- a/content/plus-checkout.js +++ b/content/plus-checkout.js @@ -114,7 +114,7 @@ async function handlePlusCheckoutCommand(message) { case 'PLUS_CHECKOUT_CLICK_SUBSCRIBE': return clickPlusSubscribe(message.payload || {}); case 'PLUS_CHECKOUT_GET_STATE': - return inspectPlusCheckoutState(); + return inspectPlusCheckoutState(message.payload || {}); default: throw new Error(`plus-checkout.js 不处理消息:${message.type}`); } @@ -1500,9 +1500,20 @@ async function clickPlusSubscribe(payload = {}) { }; } -function inspectPlusCheckoutState() { - const structuredAddress = getStructuredAddressFields(); +async function readChatGptSessionAccessToken() { + const sessionResponse = await fetch('/api/auth/session', { + credentials: 'include', + }); + const session = await sessionResponse.json().catch(() => ({})); return { + session, + accessToken: String(session?.accessToken || '').trim(), + }; +} + +async function inspectPlusCheckoutState(options = {}) { + const structuredAddress = getStructuredAddressFields(); + const state = { url: location.href, readyState: document.readyState, countryText: readCountryText(), @@ -1522,5 +1533,11 @@ function inspectPlusCheckoutState() { postalCode: structuredAddress.postalCode?.value || '', }, }; + if (options.includeSession || options.includeAccessToken) { + const sessionState = await readChatGptSessionAccessToken(); + state.session = sessionState.session; + state.accessToken = sessionState.accessToken; + } + return state; } })(); diff --git a/data/step-definitions.js b/data/step-definitions.js index f2b14ca..7d800cb 100644 --- a/data/step-definitions.js +++ b/data/step-definitions.js @@ -3,6 +3,7 @@ })(typeof self !== 'undefined' ? self : globalThis, function createStepDefinitionsModule() { const PLUS_PAYMENT_METHOD_PAYPAL = 'paypal'; const PLUS_PAYMENT_METHOD_GOPAY = 'gopay'; + const PLUS_PAYMENT_METHOD_GPC_HELPER = 'gpc-helper'; const PLUS_PAYMENT_STEP_KEY = 'paypal-approve'; const SIGNUP_METHOD_EMAIL = 'email'; const SIGNUP_METHOD_PHONE = 'phone'; @@ -50,6 +51,20 @@ { id: 13, order: 130, key: 'platform-verify', title: '平台回调验证' }, ]; + const PLUS_GPC_STEP_DEFINITIONS = [ + { id: 1, order: 10, key: 'open-chatgpt', title: '打开 ChatGPT 官网' }, + { id: 2, order: 20, key: 'submit-signup-email', title: '注册并输入邮箱' }, + { id: 3, order: 30, key: 'fill-password', title: '填写密码并继续' }, + { id: 4, order: 40, key: 'fetch-signup-code', title: '获取注册验证码' }, + { id: 5, order: 50, key: 'fill-profile', title: '填写姓名和生日' }, + { id: 6, order: 60, key: 'plus-checkout-create', title: '创建 GPC 订单' }, + { id: 7, order: 70, key: 'plus-checkout-billing', title: 'GPC OTP/PIN 验证' }, + { id: 10, order: 100, key: 'oauth-login', title: '刷新 OAuth 并登录' }, + { id: 11, order: 110, key: 'fetch-login-code', title: '获取登录验证码' }, + { id: 12, order: 120, key: 'confirm-oauth', title: '自动确认 OAuth' }, + { id: 13, order: 130, key: 'platform-verify', title: '平台回调验证' }, + ]; + const PHONE_SIGNUP_TITLE_OVERRIDES = Object.freeze({ 'submit-signup-email': '注册并输入手机号', 'fetch-signup-code': '获取手机验证码', @@ -60,9 +75,11 @@ } function normalizePlusPaymentMethod(value = '') { - return String(value || '').trim().toLowerCase() === PLUS_PAYMENT_METHOD_GOPAY - ? PLUS_PAYMENT_METHOD_GOPAY - : PLUS_PAYMENT_METHOD_PAYPAL; + 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 normalizeSignupMethod(value = '') { @@ -79,9 +96,11 @@ if (!isPlusModeEnabled(options)) { return NORMAL_STEP_DEFINITIONS; } - return normalizePlusPaymentMethod(options?.plusPaymentMethod || options?.paymentMethod) === PLUS_PAYMENT_METHOD_GOPAY - ? PLUS_GOPAY_STEP_DEFINITIONS - : PLUS_PAYPAL_STEP_DEFINITIONS; + const paymentMethod = normalizePlusPaymentMethod(options?.plusPaymentMethod || options?.paymentMethod); + if (paymentMethod === PLUS_PAYMENT_METHOD_GPC_HELPER) { + return PLUS_GPC_STEP_DEFINITIONS; + } + return paymentMethod === PLUS_PAYMENT_METHOD_GOPAY ? PLUS_GOPAY_STEP_DEFINITIONS : PLUS_PAYPAL_STEP_DEFINITIONS; } function getPlusPaymentStepTitle(options = {}) { @@ -123,6 +142,7 @@ ...NORMAL_STEP_DEFINITIONS, ...PLUS_PAYPAL_STEP_DEFINITIONS, ...PLUS_GOPAY_STEP_DEFINITIONS, + ...PLUS_GPC_STEP_DEFINITIONS, ]) { keyed.set(`${step.id}:${step.key}`, step); } @@ -158,6 +178,7 @@ PLUS_STEP_DEFINITIONS: PLUS_PAYPAL_STEP_DEFINITIONS, PLUS_PAYPAL_STEP_DEFINITIONS, PLUS_GOPAY_STEP_DEFINITIONS, + PLUS_GPC_STEP_DEFINITIONS, SIGNUP_METHOD_EMAIL, SIGNUP_METHOD_PHONE, getAllSteps, diff --git a/gopay-utils.js b/gopay-utils.js index 995ee00..a24ef8d 100644 --- a/gopay-utils.js +++ b/gopay-utils.js @@ -3,11 +3,14 @@ })(typeof self !== 'undefined' ? self : globalThis, function createGoPayUtils() { const PLUS_PAYMENT_METHOD_PAYPAL = 'paypal'; const PLUS_PAYMENT_METHOD_GOPAY = 'gopay'; + const PLUS_PAYMENT_METHOD_GPC_HELPER = 'gpc-helper'; function normalizePlusPaymentMethod(value = '') { - return String(value || '').trim().toLowerCase() === PLUS_PAYMENT_METHOD_GOPAY - ? PLUS_PAYMENT_METHOD_GOPAY - : PLUS_PAYMENT_METHOD_PAYPAL; + 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; } const DEFAULT_GOPAY_COUNTRY_CODE = '+86'; @@ -42,10 +45,167 @@ return String(value || '').trim().replace(/[^\d]/g, ''); } + function normalizeGpcHelperBaseUrl(apiUrl = '') { + let normalized = String(apiUrl || '').trim(); + if (!normalized) { + return ''; + } + normalized = normalized.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; + } + + function buildGpcHelperApiUrl(apiUrl = '', path = '') { + const baseUrl = normalizeGpcHelperBaseUrl(apiUrl); + if (!baseUrl) { + return ''; + } + const normalizedPath = String(path || '').startsWith('/') ? String(path || '') : `/${String(path || '')}`; + return `${baseUrl}${normalizedPath}`; + } + + function buildGpcCardBalanceUrl(apiUrl = '', cardKey = '') { + const endpoint = buildGpcHelperApiUrl(apiUrl, '/api/card/balance'); + if (!endpoint) { + return ''; + } + return `${endpoint}?card_key=${encodeURIComponent(String(cardKey || '').trim())}`; + } + + function extractGpcResponseErrorDetail(payload = {}, status = 0) { + if (!payload || typeof payload !== 'object') { + return status ? `HTTP ${status}` : '未知错误'; + } + + const payloadText = JSON.stringify(payload).toLowerCase(); + if (/account\s+already\s+linked/i.test(payloadText)) { + return 'GOPAY已经绑了订阅,需要手动解绑'; + } + + const direct = payload.detail + ?? payload.message + ?? payload.error + ?? payload.error_description + ?? payload.reason; + if (direct !== undefined && direct !== null && String(direct).trim()) { + const directText = String(direct).trim(); + return /account\s+already\s+linked/i.test(directText) + ? 'GOPAY已经绑了订阅,需要手动解绑' + : directText; + } + + const errorMessages = payload.error_messages ?? payload.errorMessages; + if (Array.isArray(errorMessages) && errorMessages.length > 0) { + const firstMessage = String(errorMessages[0] || '').trim(); + if (/account\s+already\s+linked/i.test(firstMessage)) { + return 'GOPAY已经绑了订阅,需要手动解绑'; + } + if (firstMessage) { + return firstMessage; + } + } + + const errors = payload.errors; + if (Array.isArray(errors) && errors.length > 0) { + const first = errors[0]; + if (typeof first === 'string') { + return first.trim() || (status ? `HTTP ${status}` : '未知错误'); + } + if (first && typeof first === 'object') { + const field = Array.isArray(first.loc) ? first.loc.join('.') : String(first.field || first.path || '').trim(); + const message = String(first.msg || first.message || first.error || '').trim(); + return [field, message].filter(Boolean).join(': ') || JSON.stringify(first); + } + } + + return status ? `HTTP ${status}` : '未知错误'; + } + + function buildGpcOtpPayload(input = {}) { + const payload = { + reference_id: String(input.reference_id ?? input.referenceId ?? '').trim(), + otp: normalizeGoPayOtp(input.otp ?? input.code ?? ''), + card_key: String(input.card_key ?? input.cardKey ?? '').trim(), + }; + const flowId = String(input.flow_id ?? input.flowId ?? '').trim(); + const gopayGuid = String(input.gopay_guid ?? input.gopayGuid ?? '').trim(); + const redirectUrl = String(input.redirect_url ?? input.redirectUrl ?? '').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 payload = buildGpcOtpPayload(input); + return { ...payload, code: payload.otp }; + } + + function 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: normalizeGoPayPin(input.pin ?? ''), + card_key: String(input.card_key ?? input.cardKey ?? '').trim(), + }; + const flowId = String(input.flow_id ?? input.flowId ?? '').trim(); + const redirectUrl = String(input.redirect_url ?? input.redirectUrl ?? '').trim(); + if (flowId) payload.flow_id = flowId; + if (redirectUrl) payload.redirect_url = redirectUrl; + return payload; + } + + function buildGpcPinRetryPayload(input = {}) { + const payload = buildGpcPinPayload(input); + return { ...payload, challengeId: payload.challenge_id }; + } + + function formatGpcBalancePayload(payload = {}) { + if (!payload || typeof payload !== 'object') { + return ''; + } + const candidates = [ + payload.remaining_uses, + payload.remainingUses, + payload.balance, + payload.remaining, + payload.uses, + payload.available_uses, + payload.availableUses, + ]; + const firstValue = candidates.find((value) => value !== undefined && value !== null && String(value).trim() !== ''); + const status = String(payload.card_status || payload.cardStatus || payload.status || '').trim(); + const flowId = String(payload.flow_id || payload.flowId || '').trim(); + const parts = []; + if (firstValue !== undefined) { + parts.push(`余额 ${firstValue}`); + } + if (status) { + parts.push(`状态 ${status}`); + } + if (flowId) { + parts.push(`flow_id ${flowId}`); + } + return parts.join(','); + } + return { DEFAULT_GOPAY_COUNTRY_CODE, + PLUS_PAYMENT_METHOD_GPC_HELPER, PLUS_PAYMENT_METHOD_GOPAY, PLUS_PAYMENT_METHOD_PAYPAL, + buildGpcCardBalanceUrl, + buildGpcHelperApiUrl, + buildGpcOtpPayload, + buildGpcOtpRetryPayload, + buildGpcPinPayload, + buildGpcPinRetryPayload, + extractGpcResponseErrorDetail, + formatGpcBalancePayload, + normalizeGpcHelperBaseUrl, normalizeGoPayCountryCode, normalizeGoPayPhone, normalizeGoPayPhoneForCountry, diff --git a/sidepanel/sidepanel.html b/sidepanel/sidepanel.html index 154da40..0792333 100644 --- a/sidepanel/sidepanel.html +++ b/sidepanel/sidepanel.html @@ -247,7 +247,9 @@ + PayPal 订阅链路 @@ -260,6 +262,57 @@ + + + + +