From bab7e54384953013c8da8fd4324bfcc74396d63d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B4=E5=9C=A3=E4=BD=91?= Date: Thu, 7 May 2026 03:06:09 +0800 Subject: [PATCH 1/9] feat(gpc): align helper flow with api key queue tasks --- background.js | 136 +++- background/auto-run-controller.js | 40 ++ background/logging-status.js | 3 +- background/message-router.js | 2 +- background/steps/create-plus-checkout.js | 257 +++---- background/steps/fill-plus-checkout.js | 657 ++++++++++++++---- gopay-utils.js | 161 ++++- sidepanel/sidepanel.html | 17 +- sidepanel/sidepanel.js | 18 +- tests/auto-run-add-phone-stop.test.js | 162 ++++- ...ackground-account-history-settings.test.js | 23 +- .../background-logging-status-module.test.js | 4 + ...ckground-message-router-step2-skip.test.js | 9 +- tests/gopay-utils.test.js | 112 +-- ...us-checkout-billing-tab-resolution.test.js | 573 ++++++++++++--- tests/plus-checkout-create-wait.test.js | 111 +-- tests/sidepanel-plus-payment-method.test.js | 4 +- tests/step-definitions-module.test.js | 7 +- 18 files changed, 1730 insertions(+), 566 deletions(-) diff --git a/background.js b/background.js index cb414ab..688b0d4 100644 --- a/background.js +++ b/background.js @@ -239,7 +239,7 @@ const SUB2API_STEP1_RESPONSE_TIMEOUT_MS = 90000; const SUB2API_STEP9_RESPONSE_TIMEOUT_MS = 120000; const DEFAULT_SUB2API_URL = 'https://sub2api.hisence.fun/admin/accounts'; const DEFAULT_CODEX2API_URL = 'http://localhost:8080/admin/accounts'; -const DEFAULT_GPC_HELPER_API_URL = 'https://gopay.hwork.pro'; +const DEFAULT_GPC_HELPER_API_URL = 'https://gpc.leftcode.xyz'; const DEFAULT_SUB2API_GROUP_NAME = 'codex'; const DEFAULT_SUB2API_PROXY_NAME = ''; const DEFAULT_SUB2API_ACCOUNT_PRIORITY = 1; @@ -607,6 +607,7 @@ const PERSISTED_SETTING_DEFAULTS = { gopayOtp: '', gopayPin: '', gopayHelperApiUrl: DEFAULT_GPC_HELPER_API_URL, + gopayHelperApiKey: '', gopayHelperCardKey: '', gopayHelperPhoneNumber: '', gopayHelperCountryCode: '+86', @@ -623,6 +624,18 @@ const PERSISTED_SETTING_DEFAULTS = { gopayHelperFlowId: '', gopayHelperChallengeId: '', gopayHelperStartPayload: null, + gopayHelperTaskId: '', + gopayHelperTaskStatus: '', + gopayHelperStatusText: '', + gopayHelperRemoteStage: '', + gopayHelperApiWaitingFor: '', + gopayHelperApiInputDeadlineAt: '', + gopayHelperApiInputWaitSeconds: 0, + gopayHelperLastInputError: '', + gopayHelperOtpInvalidCount: 0, + gopayHelperFailureStage: '', + gopayHelperFailureDetail: '', + gopayHelperTaskPayload: null, gopayHelperBalance: '', gopayHelperBalancePayload: null, gopayHelperBalanceUpdatedAt: 0, @@ -767,6 +780,18 @@ const DEFAULT_STATE = { gopayHelperFlowId: '', gopayHelperChallengeId: '', gopayHelperStartPayload: null, + gopayHelperTaskId: '', + gopayHelperTaskStatus: '', + gopayHelperStatusText: '', + gopayHelperRemoteStage: '', + gopayHelperApiWaitingFor: '', + gopayHelperApiInputDeadlineAt: '', + gopayHelperApiInputWaitSeconds: 0, + gopayHelperLastInputError: '', + gopayHelperOtpInvalidCount: 0, + gopayHelperFailureStage: '', + gopayHelperFailureDetail: '', + gopayHelperTaskPayload: null, gopayHelperOrderCreatedAt: 0, gopayHelperPinPayload: null, gopayHelperResolvedOtp: '', @@ -2280,11 +2305,12 @@ function normalizePersistentSettingValue(key, value) { case 'gopayHelperApiUrl': { const defaultGpcHelperApiUrl = PERSISTED_SETTING_DEFAULTS.gopayHelperApiUrl - || (typeof DEFAULT_GPC_HELPER_API_URL !== 'undefined' ? DEFAULT_GPC_HELPER_API_URL : 'https://gopay.hwork.pro'); + || (typeof DEFAULT_GPC_HELPER_API_URL !== 'undefined' ? DEFAULT_GPC_HELPER_API_URL : 'https://gpc.leftcode.xyz'); return self.GoPayUtils?.normalizeGpcHelperBaseUrl ? self.GoPayUtils.normalizeGpcHelperBaseUrl(value || defaultGpcHelperApiUrl) : String(value || defaultGpcHelperApiUrl).trim().replace(/\/+$/g, ''); } + case 'gopayHelperApiKey': case 'gopayHelperCardKey': case 'gopayHelperReferenceId': case 'gopayHelperGoPayGuid': @@ -2292,13 +2318,25 @@ function normalizePersistentSettingValue(key, value) { case 'gopayHelperNextAction': case 'gopayHelperFlowId': case 'gopayHelperChallengeId': + case 'gopayHelperTaskId': + case 'gopayHelperTaskStatus': + case 'gopayHelperStatusText': + case 'gopayHelperRemoteStage': + case 'gopayHelperApiWaitingFor': + case 'gopayHelperApiInputDeadlineAt': + case 'gopayHelperLastInputError': + case 'gopayHelperFailureStage': + case 'gopayHelperFailureDetail': case 'gopayHelperBalance': case 'gopayHelperBalanceError': return String(value || '').trim(); case 'gopayHelperBalancePayload': case 'gopayHelperStartPayload': + case 'gopayHelperTaskPayload': return value && typeof value === 'object' && !Array.isArray(value) ? value : null; case 'gopayHelperBalanceUpdatedAt': + case 'gopayHelperApiInputWaitSeconds': + case 'gopayHelperOtpInvalidCount': return Math.max(0, Number(value) || 0); case 'autoRunSkipFailures': case 'oauthFlowTimeoutEnabled': @@ -7268,7 +7306,8 @@ function getErrorMessage(error) { if (typeof loggingStatus !== 'undefined' && loggingStatus?.getErrorMessage) { return loggingStatus.getErrorMessage(error); } - return String(typeof error === 'string' ? error : error?.message || ''); + return String(typeof error === 'string' ? error : error?.message || '') + .replace(/^GPC_TASK_ENDED::/i, ''); } function isCloudflareSecurityBlockedError(error) { @@ -7479,6 +7518,11 @@ function isPlusCheckoutNonFreeTrialFailure(error) { return /PLUS_CHECKOUT_NON_FREE_TRIAL::|今日应付金额不是\s*0|没有免费试用资格/i.test(message); } +function isGpcTaskEndedFailure(error) { + const message = String(typeof error === 'string' ? error : error?.message || ''); + return /GPC_TASK_ENDED::/i.test(message); +} + function isGoPayCheckoutRestartRequiredFailure(error) { const message = getErrorMessage(error); return /GOPAY_RESTART_FROM_STEP6::|GOPAY_RETRY_REQUIRED::/i.test(message); @@ -7549,6 +7593,18 @@ function getDownstreamStateResets(step, state = {}) { gopayHelperFlowId: '', gopayHelperChallengeId: '', gopayHelperStartPayload: null, + gopayHelperTaskId: '', + gopayHelperTaskStatus: '', + gopayHelperStatusText: '', + gopayHelperRemoteStage: '', + gopayHelperApiWaitingFor: '', + gopayHelperApiInputDeadlineAt: '', + gopayHelperApiInputWaitSeconds: 0, + gopayHelperLastInputError: '', + gopayHelperOtpInvalidCount: 0, + gopayHelperFailureStage: '', + gopayHelperFailureDetail: '', + gopayHelperTaskPayload: null, gopayHelperOrderCreatedAt: 0, gopayHelperPinPayload: null, gopayHelperResolvedOtp: '', @@ -7638,6 +7694,7 @@ function getDownstreamStateResets(step, state = {}) { plusManualConfirmationTitle: '', plusManualConfirmationMessage: '', gopayHelperResolvedOtp: '', + gopayHelperLastInputError: '', gopayHelperOtpRequestId: '', gopayHelperOtpReferenceId: '', } : {}), @@ -9598,22 +9655,42 @@ function resolveGpcHelperBaseUrl(apiUrl = '') { let normalized = String(apiUrl || DEFAULT_GPC_HELPER_API_URL).trim().replace(/\/+$/g, ''); normalized = normalized.replace(/\/api\/checkout\/start$/i, ''); normalized = normalized.replace(/\/api\/gopay\/(?:otp|pin)$/i, ''); + normalized = normalized.replace(/\/api\/gp\/tasks(?:\/[^/?#]+)?(?:\/(?:otp|pin|stop))?(?:\?.*)?$/i, ''); + normalized = normalized.replace(/\/api\/gp\/balance(?:\?.*)?$/i, ''); normalized = normalized.replace(/\/api\/card\/balance(?:\?.*)?$/i, ''); + normalized = normalized.replace(/\/api\/card\/redeem-api-key(?:\?.*)?$/i, ''); return normalized || DEFAULT_GPC_HELPER_API_URL; } -function buildGpcCardBalanceRequestUrl(apiUrl = '', cardKey = '') { +function buildGpcApiKeyBalanceRequestUrl(apiUrl = '') { + if (self.GoPayUtils?.buildGpcApiKeyBalanceUrl) { + return self.GoPayUtils.buildGpcApiKeyBalanceUrl(apiUrl); + } if (self.GoPayUtils?.buildGpcCardBalanceUrl) { - return self.GoPayUtils.buildGpcCardBalanceUrl(apiUrl, cardKey); + return self.GoPayUtils.buildGpcCardBalanceUrl(apiUrl); } const baseUrl = resolveGpcHelperBaseUrl(apiUrl); if (!baseUrl) { return ''; } - return `${baseUrl}/api/card/balance?card_key=${encodeURIComponent(String(cardKey || '').trim())}`; + return `${baseUrl}/api/gp/balance`; } -function formatGpcCardBalancePayload(payload = {}) { +function buildGpcApiKeyHeaders(apiKey = '', extraHeaders = {}) { + if (self.GoPayUtils?.buildGpcApiKeyHeaders) { + return self.GoPayUtils.buildGpcApiKeyHeaders(apiKey, extraHeaders); + } + const headers = { + ...(extraHeaders && typeof extraHeaders === 'object' ? extraHeaders : {}), + }; + const normalizedApiKey = String(apiKey || '').trim(); + if (normalizedApiKey) { + headers['X-API-Key'] = normalizedApiKey; + } + return headers; +} + +function formatGpcApiKeyBalancePayload(payload = {}) { if (self.GoPayUtils?.formatGpcBalancePayload) { return self.GoPayUtils.formatGpcBalancePayload(payload); } @@ -9621,30 +9698,40 @@ function formatGpcCardBalancePayload(payload = {}) { return ''; } const remaining = payload.remaining_uses ?? payload.remainingUses ?? payload.balance ?? payload.remaining; + const total = payload.total_uses ?? payload.totalUses; + const used = payload.used_uses ?? payload.usedUses; const status = String(payload.card_status || payload.cardStatus || payload.status || '').trim(); return [ - remaining !== undefined && remaining !== null && String(remaining).trim() !== '' ? `余额 ${remaining}` : '', + remaining !== undefined && remaining !== null && String(remaining).trim() !== '' + ? (total !== undefined && total !== null && String(total).trim() !== '' ? `余额 ${remaining}/${total}` : `余额 ${remaining}`) + : '', + used !== undefined && used !== null && String(used).trim() !== '' ? `已用 ${used}` : '', status ? `状态 ${status}` : '', ].filter(Boolean).join(','); } -async function refreshGpcCardBalance(state = {}, options = {}) { +async function refreshGpcApiKeyBalance(state = {}, options = {}) { const apiUrl = resolveGpcHelperBaseUrl(state?.gopayHelperApiUrl || DEFAULT_GPC_HELPER_API_URL); - const cardKey = String(state?.gopayHelperCardKey || state?.gpcCardKey || state?.cardKey || '').trim(); + const apiKey = String( + state?.gopayHelperApiKey + || state?.gpcApiKey + || state?.apiKey + || '' + ).trim(); if (!apiUrl) { throw new Error('缺少 GPC API 地址。'); } - if (!cardKey) { - throw new Error('缺少 GPC 卡密。'); + if (!apiKey) { + throw new Error('缺少 GPC API Key。'); } - const requestUrl = buildGpcCardBalanceRequestUrl(apiUrl, cardKey); + const requestUrl = buildGpcApiKeyBalanceRequestUrl(apiUrl); if (!requestUrl) { throw new Error('缺少 GPC API 地址。'); } const response = await fetch(requestUrl, { method: 'GET', - headers: { Accept: 'application/json' }, + headers: buildGpcApiKeyHeaders(apiKey, { Accept: 'application/json' }), }); const rawText = await response.text(); let payload = {}; @@ -9653,20 +9740,28 @@ async function refreshGpcCardBalance(state = {}, options = {}) { } catch { payload = { raw: rawText }; } - const balanceText = formatGpcCardBalancePayload(payload) || rawText || '未知'; + const balancePayload = self.GoPayUtils?.unwrapGpcResponse + ? self.GoPayUtils.unwrapGpcResponse(payload) + : (payload?.data && typeof payload === 'object' ? payload.data : payload); + const balanceText = formatGpcApiKeyBalancePayload(payload) || rawText || '未知'; const updates = { gopayHelperBalance: balanceText, - gopayHelperBalancePayload: payload && typeof payload === 'object' && !Array.isArray(payload) ? payload : { raw: String(payload || '') }, + gopayHelperBalancePayload: balancePayload && typeof balancePayload === 'object' && !Array.isArray(balancePayload) ? balancePayload : { raw: String(balancePayload || '') }, gopayHelperBalanceUpdatedAt: Date.now(), gopayHelperBalanceError: '', }; - const flowId = String(payload?.flow_id || payload?.flowId || '').trim(); + const flowId = String(balancePayload?.flow_id || balancePayload?.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 unifiedOk = self.GoPayUtils?.isGpcUnifiedResponseOk + ? self.GoPayUtils.isGpcUnifiedResponseOk(payload) + : true; + if (!response.ok || payload?.ok === false || !unifiedOk) { + const detail = self.GoPayUtils?.extractGpcResponseErrorDetail + ? self.GoPayUtils.extractGpcResponseErrorDetail(payload, response.status) + : (payload?.data?.detail || payload?.error || payload?.message || payload?.detail || `HTTP ${response.status}`); const errorUpdates = { ...updates, gopayHelperBalanceError: String(detail || '余额查询失败') }; await setPersistentSettings(errorUpdates); broadcastDataUpdate(errorUpdates); @@ -9685,6 +9780,8 @@ async function refreshGpcCardBalance(state = {}, options = {}) { return { balance: balanceText, payload, updatedAt: updates.gopayHelperBalanceUpdatedAt }; } +const refreshGpcCardBalance = refreshGpcApiKeyBalance; + const autoRunController = self.MultiPageBackgroundAutoRunController?.createAutoRunController({ addLog, appendAccountRunRecord: (...args) => appendAndBroadcastAccountRunRecord(...args), @@ -9709,6 +9806,7 @@ const autoRunController = self.MultiPageBackgroundAutoRunController?.createAutoR isAddPhoneAuthFailure, isPhoneSmsPlatformRateLimitFailure, isPlusCheckoutNonFreeTrialFailure, + isGpcTaskEndedFailure, isRestartCurrentAttemptError, isStep4Route405RecoveryLimitFailure, isSignupUserAlreadyExistsFailure, diff --git a/background/auto-run-controller.js b/background/auto-run-controller.js index 7abc462..7c9e063 100644 --- a/background/auto-run-controller.js +++ b/background/auto-run-controller.js @@ -23,6 +23,7 @@ getState, hasSavedProgress, isAddPhoneAuthFailure, + isGpcTaskEndedFailure, isPhoneSmsPlatformRateLimitFailure, isPlusCheckoutNonFreeTrialFailure, isRestartCurrentAttemptError, @@ -529,6 +530,9 @@ && isAddPhoneAuthFailure(err); const blockedByPlusNonFreeTrial = typeof isPlusCheckoutNonFreeTrialFailure === 'function' && isPlusCheckoutNonFreeTrialFailure(err); + const blockedByGpcTaskEnded = typeof isGpcTaskEndedFailure === 'function' + ? isGpcTaskEndedFailure(err) + : /GPC_TASK_ENDED::/i.test(err?.message || String(err || '')); const blockedBySignupUserAlreadyExists = typeof isSignupUserAlreadyExistsFailure === 'function' && !keepSameEmailUntilAddPhone && isSignupUserAlreadyExistsFailure(err); @@ -537,6 +541,7 @@ const canRetry = !blockedByAddPhone && !blockedByPhoneNoSupply && !blockedByPlusNonFreeTrial + && !blockedByGpcTaskEnded && !blockedBySignupUserAlreadyExists && autoRunSkipFailures && attemptRun < maxAttemptsForRound; @@ -650,6 +655,41 @@ break; } + if (blockedByGpcTaskEnded) { + roundSummary.status = 'failed'; + roundSummary.finalFailureReason = reason; + await setState({ + autoRunRoundSummaries: serializeAutoRunRoundSummaries(totalRuns, roundSummaries), + }); + await appendRoundRecordIfNeeded('failed', reason); + cancelPendingCommands('当前轮因 GPC 任务已结束。'); + await broadcastStopToContentScripts(); + if (!autoRunSkipFailures) { + await addLog( + `第 ${targetRun}/${totalRuns} 轮 GPC 任务已结束,自动重试未开启,当前自动运行将停止。`, + 'warn' + ); + stoppedEarly = true; + await broadcastAutoRunStatus('stopped', { + currentRun: targetRun, + totalRuns, + attemptRun, + sessionId: 0, + }); + break; + } + + await addLog(`第 ${targetRun}/${totalRuns} 轮 GPC 任务已结束,本轮将直接失败并跳过剩余重试。`, 'warn'); + await addLog( + targetRun < totalRuns + ? `第 ${targetRun}/${totalRuns} 轮因 GPC 任务结束提前结束,自动流程将继续下一轮。` + : `第 ${targetRun}/${totalRuns} 轮因 GPC 任务结束提前结束,已无后续轮次,本次自动运行结束。`, + 'warn' + ); + forceFreshTabsNextRun = true; + break; + } + if (blockedBySignupUserAlreadyExists) { roundSummary.status = 'failed'; roundSummary.finalFailureReason = reason; diff --git a/background/logging-status.js b/background/logging-status.js index bc1b3a5..f6a21e4 100644 --- a/background/logging-status.js +++ b/background/logging-status.js @@ -71,7 +71,8 @@ } function getErrorMessage(error) { - return String(typeof error === 'string' ? error : error?.message || ''); + return String(typeof error === 'string' ? error : error?.message || '') + .replace(/^GPC_TASK_ENDED::/i, ''); } function isVerificationMailPollingError(error) { diff --git a/background/message-router.js b/background/message-router.js index 9960bde..ca8b3fb 100644 --- a/background/message-router.js +++ b/background/message-router.js @@ -1031,7 +1031,7 @@ case 'REFRESH_GPC_CARD_BALANCE': { if (typeof refreshGpcCardBalance !== 'function') { - throw new Error('GPC 卡密余额查询能力尚未接入。'); + throw new Error('GPC API Key 余额查询能力尚未接入。'); } const state = await getState(); const result = await refreshGpcCardBalance({ diff --git a/background/steps/create-plus-checkout.js b/background/steps/create-plus-checkout.js index 4914884..f1ee9f5 100644 --- a/background/steps/create-plus-checkout.js +++ b/background/steps/create-plus-checkout.js @@ -7,7 +7,7 @@ const PLUS_PAYMENT_METHOD_PAYPAL = 'paypal'; const PLUS_PAYMENT_METHOD_GOPAY = 'gopay'; const PLUS_PAYMENT_METHOD_GPC_HELPER = 'gpc-helper'; - const DEFAULT_GPC_HELPER_API_URL = 'https://gopay.hwork.pro'; + const DEFAULT_GPC_HELPER_API_URL = 'https://gpc.leftcode.xyz'; function createPlusCheckoutCreateExecutor(deps = {}) { const { @@ -16,7 +16,6 @@ completeStepFromBackground, ensureContentScriptReadyOnTabUntilStopped, fetch: fetchImpl = null, - markCurrentRegistrationAccountUsed = null, registerTab, sendTabMessageUntilStopped, setState, @@ -95,121 +94,17 @@ return String(value || '').trim().toLowerCase() === 'sms' ? 'sms' : 'whatsapp'; } - 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 + function resolveGpcHelperApiKey(state = {}) { + const apiKey = String( + state?.gopayHelperApiKey + || state?.gpcApiKey + || state?.apiKey || '' - ).trim().toLowerCase(); - if (!email) { - throw new Error('创建 GPC 订单失败:缺少当前轮邮箱。'); + ).trim(); + if (!apiKey) { + throw new Error('创建 GPC 订单失败:缺少 API Key。'); } - 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}),当前账号没有免费试用资格,已跳过支付提交。`); + return apiKey; } function normalizeGpcHelperBaseUrl(apiUrl = '') { @@ -220,7 +115,10 @@ let normalized = String(apiUrl || DEFAULT_GPC_HELPER_API_URL).trim().replace(/\/+$/g, ''); normalized = normalized.replace(/\/api\/checkout\/start$/i, ''); normalized = normalized.replace(/\/api\/gopay\/(?:otp|pin)$/i, ''); + normalized = normalized.replace(/\/api\/gp\/tasks(?:\/[^/?#]+)?(?:\/(?:otp|pin|stop))?(?:\?.*)?$/i, ''); + normalized = normalized.replace(/\/api\/gp\/balance(?:\?.*)?$/i, ''); normalized = normalized.replace(/\/api\/card\/balance(?:\?.*)?$/i, ''); + normalized = normalized.replace(/\/api\/card\/redeem-api-key(?:\?.*)?$/i, ''); return normalized || DEFAULT_GPC_HELPER_API_URL; } @@ -237,6 +135,47 @@ return `${baseUrl}${normalizedPath}`; } + function buildGpcTaskCreateUrl(apiUrl = '') { + const rootScope = typeof self !== 'undefined' ? self : globalThis; + if (rootScope.GoPayUtils?.buildGpcTaskCreateUrl) { + return rootScope.GoPayUtils.buildGpcTaskCreateUrl(apiUrl); + } + return buildGpcHelperApiUrl(apiUrl, '/api/gp/tasks'); + } + + function unwrapGpcResponse(payload = {}) { + const rootScope = typeof self !== 'undefined' ? self : globalThis; + if (rootScope.GoPayUtils?.unwrapGpcResponse) { + return rootScope.GoPayUtils.unwrapGpcResponse(payload); + } + if (payload && typeof payload === 'object' && !Array.isArray(payload) + && Object.prototype.hasOwnProperty.call(payload, 'data') + && (Object.prototype.hasOwnProperty.call(payload, 'code') || Object.prototype.hasOwnProperty.call(payload, 'message'))) { + return payload.data ?? {}; + } + return payload; + } + + function isGpcUnifiedResponseOk(payload = {}) { + const rootScope = typeof self !== 'undefined' ? self : globalThis; + if (rootScope.GoPayUtils?.isGpcUnifiedResponseOk) { + return rootScope.GoPayUtils.isGpcUnifiedResponseOk(payload); + } + if (!payload || typeof payload !== 'object' || !Object.prototype.hasOwnProperty.call(payload, 'code')) { + return true; + } + const code = Number(payload.code); + return Number.isFinite(code) ? code >= 200 && code < 300 : String(payload.code || '').trim() === '200'; + } + + function getGpcResponseErrorDetail(payload = {}, status = 0) { + const rootScope = typeof self !== 'undefined' ? self : globalThis; + if (rootScope.GoPayUtils?.extractGpcResponseErrorDetail) { + return rootScope.GoPayUtils.extractGpcResponseErrorDetail(payload, status); + } + return payload?.data?.detail || payload?.detail || payload?.message || payload?.error || `HTTP ${status || 0}`; + } + async function fetchJsonWithTimeout(url, options = {}, timeoutMs = 30000) { const fetcher = typeof fetchImpl === 'function' ? fetchImpl @@ -283,14 +222,14 @@ if (!token) { throw new Error('创建 GPC 订单失败:缺少 accessToken。'); } - const apiUrl = buildGpcHelperApiUrl(state?.gopayHelperApiUrl, '/api/checkout/start'); + const apiUrl = buildGpcTaskCreateUrl(state?.gopayHelperApiUrl); 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); + const apiKey = resolveGpcHelperApiKey(state); if (!phoneNumber) { throw new Error('创建 GPC 订单失败:缺少手机号。'); } @@ -300,32 +239,11 @@ 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), - phone_mode: 'manual', - otp_channel: normalizeGpcOtpChannel(state?.gopayHelperOtpChannel), - }, + access_token: token, + phone_mode: 'manual', + country_code: countryCode, + phone_number: normalizeHelperPhoneNumber(phoneNumber, countryCode), + otp_channel: normalizeGpcOtpChannel(state?.gopayHelperOtpChannel), }; const orderCreatedAt = Date.now(); @@ -335,38 +253,26 @@ Accept: '*/*', 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8', 'Content-Type': 'application/json', + 'X-API-Key': apiKey, }, 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(); + const taskData = unwrapGpcResponse(data); + const taskId = String(taskData?.task_id || taskData?.taskId || '').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}`); + if (!response?.ok || !isGpcUnifiedResponseOk(data) || !taskId) { + const detail = getGpcResponseErrorDetail(data, response?.status || 0); throw new Error(`创建 GPC 订单失败:${detail}`); } return { - referenceId, - gopayGuid, - redirectUrl, - nextAction, - flowId, - challengeId, + taskId, + taskStatus: String(taskData?.status || '').trim(), + statusText: String(taskData?.status_text || taskData?.statusText || '').trim(), + remoteStage: String(taskData?.remote_stage || taskData?.remoteStage || '').trim(), orderCreatedAt, - responsePayload: data && typeof data === 'object' && !Array.isArray(data) ? data : null, + responsePayload: taskData && typeof taskData === 'object' && !Array.isArray(taskData) ? taskData : null, country: 'ID', currency: 'IDR', checkoutSource: PLUS_PAYMENT_METHOD_GPC_HELPER, @@ -398,16 +304,21 @@ 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, + gopayHelperTaskId: result.taskId, + gopayHelperTaskStatus: result.taskStatus, + gopayHelperStatusText: result.statusText, + gopayHelperRemoteStage: result.remoteStage, + gopayHelperTaskPayload: result.responsePayload, + gopayHelperReferenceId: '', + gopayHelperGoPayGuid: '', + gopayHelperRedirectUrl: '', + gopayHelperNextAction: '', + gopayHelperFlowId: '', + gopayHelperChallengeId: '', + gopayHelperStartPayload: null, gopayHelperOrderCreatedAt: result.orderCreatedAt || Date.now(), }); - await addLog('步骤 6:GPC 订单已创建,准备继续下一步。', 'info'); + await addLog(`步骤 6:GPC 任务已创建(task_id: ${result.taskId}),准备继续下一步。`, 'info'); await completeStepFromBackground(6, { plusCheckoutCountry: result.country || 'ID', plusCheckoutCurrency: result.currency || 'IDR', diff --git a/background/steps/fill-plus-checkout.js b/background/steps/fill-plus-checkout.js index 78168f2..2952b96 100644 --- a/background/steps/fill-plus-checkout.js +++ b/background/steps/fill-plus-checkout.js @@ -10,7 +10,8 @@ const PLUS_PAYMENT_METHOD_PAYPAL = 'paypal'; const PLUS_PAYMENT_METHOD_GOPAY = 'gopay'; const PLUS_PAYMENT_METHOD_GPC_HELPER = 'gpc-helper'; - const DEFAULT_GPC_HELPER_API_URL = 'https://gopay.hwork.pro'; + const DEFAULT_GPC_HELPER_API_URL = 'https://gpc.leftcode.xyz'; + const GPC_TASK_POLL_INTERVAL_MS = 3000; const PAYMENT_METHOD_CONFIGS = { [PLUS_PAYMENT_METHOD_PAYPAL]: { id: PLUS_PAYMENT_METHOD_PAYPAL, @@ -92,7 +93,7 @@ function isGpcHelperCheckout(state = {}) { return normalizePlusPaymentMethod(state?.plusPaymentMethod) === PLUS_PAYMENT_METHOD_GPC_HELPER || (normalizeText(state?.plusCheckoutSource) === PLUS_PAYMENT_METHOD_GPC_HELPER - && Boolean(state?.gopayHelperReferenceId)); + && Boolean(state?.gopayHelperTaskId || state?.gopayHelperReferenceId)); } function compactCountryText(value = '') { @@ -123,7 +124,10 @@ let normalized = String(apiUrl || DEFAULT_GPC_HELPER_API_URL).trim().replace(/\/+$/g, ''); normalized = normalized.replace(/\/api\/checkout\/start$/i, ''); normalized = normalized.replace(/\/api\/gopay\/(?:otp|pin)$/i, ''); + normalized = normalized.replace(/\/api\/gp\/tasks(?:\/[^/?#]+)?(?:\/(?:otp|pin|stop))?(?:\?.*)?$/i, ''); + normalized = normalized.replace(/\/api\/gp\/balance(?:\?.*)?$/i, ''); normalized = normalized.replace(/\/api\/card\/balance(?:\?.*)?$/i, ''); + normalized = normalized.replace(/\/api\/card\/redeem-api-key(?:\?.*)?$/i, ''); return normalized || DEFAULT_GPC_HELPER_API_URL; } @@ -153,7 +157,6 @@ 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(); @@ -183,7 +186,6 @@ 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(); @@ -207,11 +209,160 @@ 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 payload?.data?.detail || payload.detail || payload.message || payload.error || payload.error_description || payload.reason || `HTTP ${status || 0}`; } return `HTTP ${status || 0}`; } + function unwrapGpcResponse(payload = {}) { + const rootScope = typeof self !== 'undefined' ? self : globalThis; + if (rootScope.GoPayUtils?.unwrapGpcResponse) { + return rootScope.GoPayUtils.unwrapGpcResponse(payload); + } + if (payload && typeof payload === 'object' && !Array.isArray(payload) + && Object.prototype.hasOwnProperty.call(payload, 'data') + && (Object.prototype.hasOwnProperty.call(payload, 'code') || Object.prototype.hasOwnProperty.call(payload, 'message'))) { + return payload.data ?? {}; + } + return payload; + } + + function isGpcUnifiedResponseOk(payload = {}) { + const rootScope = typeof self !== 'undefined' ? self : globalThis; + if (rootScope.GoPayUtils?.isGpcUnifiedResponseOk) { + return rootScope.GoPayUtils.isGpcUnifiedResponseOk(payload); + } + if (!payload || typeof payload !== 'object' || !Object.prototype.hasOwnProperty.call(payload, 'code')) { + return true; + } + const code = Number(payload.code); + return Number.isFinite(code) ? code >= 200 && code < 300 : String(payload.code || '').trim() === '200'; + } + + function buildGpcTaskQueryUrl(apiUrl = '', taskId = '') { + const rootScope = typeof self !== 'undefined' ? self : globalThis; + if (rootScope.GoPayUtils?.buildGpcTaskQueryUrl) { + return rootScope.GoPayUtils.buildGpcTaskQueryUrl(apiUrl, taskId); + } + const baseUrl = normalizeGpcHelperBaseUrl(apiUrl); + return `${baseUrl}/api/gp/tasks/${encodeURIComponent(String(taskId || '').trim())}`; + } + + function buildGpcTaskActionUrl(apiUrl = '', taskId = '', action = '') { + const rootScope = typeof self !== 'undefined' ? self : globalThis; + if (rootScope.GoPayUtils?.buildGpcTaskActionUrl) { + return rootScope.GoPayUtils.buildGpcTaskActionUrl(apiUrl, taskId, action); + } + const baseUrl = normalizeGpcHelperBaseUrl(apiUrl); + const normalizedAction = String(action || '').trim().replace(/^\/+|\/+$/g, ''); + return `${baseUrl}/api/gp/tasks/${encodeURIComponent(String(taskId || '').trim())}/${normalizedAction}`; + } + + function buildGpcTaskOtpPayload(input = {}) { + const rootScope = typeof self !== 'undefined' ? self : globalThis; + if (rootScope.GoPayUtils?.buildGpcTaskOtpPayload) { + return rootScope.GoPayUtils.buildGpcTaskOtpPayload(input); + } + return { + otp: String(input.otp ?? input.code ?? '').trim().replace(/[^\d]/g, ''), + }; + } + + function buildGpcTaskPinPayload(input = {}) { + const rootScope = typeof self !== 'undefined' ? self : globalThis; + if (rootScope.GoPayUtils?.buildGpcTaskPinPayload) { + return rootScope.GoPayUtils.buildGpcTaskPinPayload(input); + } + return { + pin: String(input.pin ?? '').trim().replace(/[^\d]/g, ''), + }; + } + + function buildGpcApiKeyHeaders(apiKey = '', extraHeaders = {}) { + const rootScope = typeof self !== 'undefined' ? self : globalThis; + if (rootScope.GoPayUtils?.buildGpcApiKeyHeaders) { + return rootScope.GoPayUtils.buildGpcApiKeyHeaders(apiKey, extraHeaders); + } + const headers = { + ...(extraHeaders && typeof extraHeaders === 'object' ? extraHeaders : {}), + }; + const normalizedApiKey = String(apiKey || '').trim(); + if (normalizedApiKey) { + headers['X-API-Key'] = normalizedApiKey; + } + return headers; + } + + function normalizeGpcTaskData(payload = {}) { + const data = unwrapGpcResponse(payload); + const task = data && typeof data === 'object' && !Array.isArray(data) ? { ...data } : {}; + task.task_id = String(task.task_id || task.taskId || '').trim(); + task.status = String(task.status || '').trim().toLowerCase(); + task.status_text = String(task.status_text || task.statusText || '').trim(); + task.phone_mode = String(task.phone_mode || task.phoneMode || '').trim().toLowerCase(); + task.remote_stage = String(task.remote_stage || task.remoteStage || '').trim().toLowerCase(); + task.api_waiting_for = String(task.api_waiting_for || task.apiWaitingFor || '').trim().toLowerCase(); + task.api_input_deadline_at = String(task.api_input_deadline_at || task.apiInputDeadlineAt || '').trim(); + task.api_input_wait_seconds = Math.max(0, Number(task.api_input_wait_seconds ?? task.apiInputWaitSeconds) || 0); + task.last_input_error = String(task.last_input_error || task.lastInputError || '').trim(); + task.otp_invalid_count = Math.max(0, Number(task.otp_invalid_count ?? task.otpInvalidCount) || 0); + task.failure_stage = String(task.failure_stage || task.failureStage || '').trim(); + task.failure_detail = String(task.failure_detail || task.failureDetail || '').trim(); + task.error_message = String(task.error_message || task.errorMessage || '').trim(); + return task; + } + + async function setGpcTaskState(taskData = {}) { + const task = normalizeGpcTaskData(taskData); + const updates = { + gopayHelperTaskId: task.task_id, + gopayHelperTaskStatus: task.status, + gopayHelperStatusText: task.status_text, + gopayHelperRemoteStage: task.remote_stage, + gopayHelperApiWaitingFor: task.api_waiting_for, + gopayHelperApiInputDeadlineAt: task.api_input_deadline_at, + gopayHelperApiInputWaitSeconds: task.api_input_wait_seconds, + gopayHelperLastInputError: task.last_input_error, + gopayHelperOtpInvalidCount: task.otp_invalid_count, + gopayHelperFailureStage: task.failure_stage, + gopayHelperFailureDetail: task.failure_detail, + gopayHelperTaskPayload: task && typeof task === 'object' && !Array.isArray(task) ? task : null, + }; + await setState(updates); + if (typeof broadcastDataUpdate === 'function') { + broadcastDataUpdate(updates); + } + return task; + } + + async function fetchGpcTaskStatus(apiUrl, taskId, apiKey) { + const requestUrl = buildGpcTaskQueryUrl(apiUrl, taskId); + const { response, data } = await fetchJsonWithTimeout(requestUrl, { + method: 'GET', + headers: buildGpcApiKeyHeaders(apiKey, { Accept: 'application/json' }), + }, 30000); + if (!response?.ok || !isGpcUnifiedResponseOk(data)) { + throw new Error(getGpcResponseErrorDetail(data, response?.status || 0)); + } + return setGpcTaskState(data); + } + + async function postGpcTaskAction(apiUrl, taskId, action, payload = {}, apiKey = '', timeoutMs = 30000) { + const requestUrl = buildGpcTaskActionUrl(apiUrl, taskId, action); + const { response, data } = await fetchJsonWithTimeout(requestUrl, { + method: 'POST', + headers: buildGpcApiKeyHeaders(apiKey, { + Accept: 'application/json', + 'Content-Type': 'application/json', + }), + body: JSON.stringify(payload), + }, timeoutMs); + if (!response?.ok || !isGpcUnifiedResponseOk(data)) { + throw new Error(getGpcResponseErrorDetail(data, response?.status || 0)); + } + return setGpcTaskState(data); + } + async function postGpcJsonWithFallback(apiUrl, endpointPath, primaryPayload, fallbackPayload, timeoutMs = 30000) { const requestUrl = `${apiUrl}${endpointPath}`; const send = (payload) => fetchJsonWithTimeout(requestUrl, { @@ -312,35 +463,42 @@ return Number.isFinite(parsed) ? parsed : 0; } - function buildLocalSmsHelperOtpUrl(state = {}, referenceId = '') { + function buildLocalSmsHelperOtpUrl(state = {}, taskId = '', options = {}) { const baseUrl = normalizeLocalSmsHelperBaseUrl(state?.gopayHelperLocalSmsHelperUrl); const url = new URL(`${baseUrl}/otp`); - const normalizedReferenceId = String(referenceId || '').trim(); + const normalizedTaskId = String(taskId || '').trim(); const phoneNumber = String(state?.gopayHelperPhoneNumber || '').trim(); + const afterOverrideMs = normalizeEpochMilliseconds(options?.afterMs || options?.after_ms || 0); const orderCreatedAt = normalizeEpochMilliseconds( state?.gopayHelperOrderCreatedAt + || state?.gopayHelperTaskPayload?.created_at + || state?.gopayHelperTaskPayload?.createdAt || state?.gopayHelperStartPayload?.order_created_at || state?.gopayHelperStartPayload?.orderCreatedAt || state?.gopayHelperStartPayload?.created_at || state?.gopayHelperStartPayload?.createdAt ); - if (normalizedReferenceId) { - url.searchParams.set('reference_id', normalizedReferenceId); + if (normalizedTaskId) { + url.searchParams.set('task_id', normalizedTaskId); + url.searchParams.set('reference_id', normalizedTaskId); } if (phoneNumber) { url.searchParams.set('phone_number', phoneNumber); } - if (orderCreatedAt > 0) { - url.searchParams.set('after_ms', String(orderCreatedAt)); + const effectiveAfterMs = Math.max(orderCreatedAt, afterOverrideMs); + if (effectiveAfterMs > 0) { + url.searchParams.set('after_ms', String(effectiveAfterMs)); } return url.toString(); } - async function pollLocalSmsHelperOtp(state = {}, referenceId = '') { - const timeoutSeconds = Math.max(10, Math.min(300, Number(state?.gopayHelperLocalSmsTimeoutSeconds) || 90)); + async function pollLocalSmsHelperOtp(state = {}, taskId = '', options = {}) { + const timeoutSeconds = Math.max(1, Math.min(300, Number(options?.timeoutSeconds ?? options?.timeout_seconds ?? state?.gopayHelperLocalSmsTimeoutSeconds) || 90)); const pollIntervalSeconds = Math.max(1, Math.min(30, Number(state?.gopayHelperLocalSmsPollIntervalSeconds) || 2)); + const singleAttempt = Boolean(options?.singleAttempt || options?.single_attempt); + const requestTimeoutMs = Math.max(1000, Math.min(8000, Number(options?.requestTimeoutMs ?? options?.request_timeout_ms) || pollIntervalSeconds * 1000)); const deadline = Date.now() + timeoutSeconds * 1000; - const requestUrl = buildLocalSmsHelperOtpUrl(state, referenceId); + const requestUrl = buildLocalSmsHelperOtpUrl(state, taskId, options); let lastMessage = ''; while (Date.now() <= deadline) { throwIfStopped(); @@ -348,7 +506,7 @@ const { response, data } = await fetchJsonWithTimeout(requestUrl, { method: 'GET', headers: { Accept: 'application/json' }, - }, Math.min(8000, Math.max(1000, pollIntervalSeconds * 1000))); + }, requestTimeoutMs); const otp = normalizeIncomingGpcSmsOtp(data || {}); if (response?.ok && otp) { await setState({ @@ -364,12 +522,56 @@ } catch (error) { lastMessage = error?.message || String(error || '未知错误'); } + if (singleAttempt) { + break; + } await sleepWithStop(pollIntervalSeconds * 1000); } throw new Error(lastMessage || '本地 SMS Helper 等待 OTP 超时。'); } - async function requestGpcOtpInput({ title = '', message = '', referenceId = '' }) { + function buildGpcTaskEndedError(task = {}, fallbackMessage = '') { + const detail = buildGpcTaskTerminalError(task) || fallbackMessage || 'GPC 任务已结束,请重新创建任务。'; + return new Error(`GPC_TASK_ENDED::${detail}`); + } + + function isGpcTaskInputDeadlineExpired(task = {}) { + const deadlineMs = normalizeEpochMilliseconds(task?.api_input_deadline_at || task?.apiInputDeadlineAt || ''); + return deadlineMs > 0 && Date.now() > deadlineMs; + } + + function buildGpcInputDeadlineError(task = {}, label = '输入') { + const stage = String(task?.remote_stage || '').trim(); + const detail = `${label}提交已超时,请重新创建任务。`; + return new Error(`GPC_TASK_ENDED::${detail}${stage ? `(${stage})` : ''}`); + } + + function normalizeSixDigitOtp(value = '') { + const otp = String(value || '').trim().replace(/[^\d]/g, ''); + return /^\d{6}$/.test(otp) ? otp : ''; + } + + function normalizeSixDigitPin(value = '') { + const pin = String(value || '').trim().replace(/[^\d]/g, ''); + return /^\d{6}$/.test(pin) ? pin : ''; + } + + function isGpcOtpFormatConflict(error) { + return /OTP\s*必须是\s*6\s*位数字|OTP.*6.*digit|task_conflict/i.test(error?.message || String(error || '')); + } + + async function requestGpcOtpInput({ title = '', message = '', taskId = '', lastInputError = '', inputDeadlineAt = '' }) { + const existingState = await getStateInternal(); + const existingRequestId = String(existingState?.plusManualConfirmationRequestId || '').trim(); + if ( + existingState?.plusManualConfirmationPending + && existingRequestId + && String(existingState?.plusManualConfirmationMethod || '').trim().toLowerCase() === 'gopay-otp' + && String(existingState?.gopayHelperOtpReferenceId || '').trim() === String(taskId || '').trim() + ) { + return waitForGpcOtpInput(existingRequestId, { inputDeadlineAt }); + } + const requestId = `otp-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; const payload = { plusManualConfirmationPending: true, @@ -378,18 +580,45 @@ plusManualConfirmationMethod: 'gopay-otp', plusManualConfirmationTitle: title || 'GPC OTP 验证', plusManualConfirmationMessage: message || '请输入 OTP 验证码', - gopayHelperOtpRequestId: requestId, - gopayHelperOtpReferenceId: referenceId, + gopayHelperLastInputError: String(lastInputError || '').trim(), + gopayHelperApiInputDeadlineAt: String(inputDeadlineAt || '').trim(), gopayHelperResolvedOtp: '', + gopayHelperOtpRequestId: requestId, + gopayHelperOtpReferenceId: taskId, }; await setState(payload); if (typeof broadcastDataUpdate === 'function') { broadcastDataUpdate(payload); } + return waitForGpcOtpInput(requestId, { inputDeadlineAt }); + } + + function waitForGpcOtpInput(requestId = '', options = {}) { + const deadlineMs = normalizeEpochMilliseconds(options?.inputDeadlineAt || options?.input_deadline_at || 0); return new Promise((resolve, reject) => { const checkInterval = setInterval(async () => { try { throwIfStopped(); + if (deadlineMs > 0 && Date.now() > deadlineMs) { + clearInterval(checkInterval); + const clearPayload = { + plusManualConfirmationPending: false, + plusManualConfirmationRequestId: '', + plusManualConfirmationStep: 0, + plusManualConfirmationMethod: '', + plusManualConfirmationTitle: '', + plusManualConfirmationMessage: '', + gopayHelperResolvedOtp: '', + gopayHelperOtpRequestId: '', + gopayHelperOtpReferenceId: '', + }; + await setState(clearPayload); + if (typeof broadcastDataUpdate === 'function') { + broadcastDataUpdate(clearPayload); + } + reject(new Error('OTP 输入超时')); + return; + } const currentState = await getStateInternal(); if (!currentState?.plusManualConfirmationPending || currentState?.plusManualConfirmationRequestId !== requestId) { clearInterval(checkInterval); @@ -408,122 +637,308 @@ }); } + function isGpcTaskOtpWait(task = {}) { + return task?.phone_mode === 'manual' && task?.api_waiting_for === 'otp'; + } + + function isGpcTaskPinWait(task = {}) { + return task?.phone_mode === 'manual' + && (task?.api_waiting_for === 'pin' || task?.status === 'otp_ready'); + } + + function isGpcTaskTerminal(status = '') { + return ['completed', 'failed', 'expired', 'discarded'].includes(String(status || '').trim().toLowerCase()); + } + + function buildGpcTaskTerminalError(task = {}) { + const status = String(task?.status || '').trim().toLowerCase(); + const remoteStage = String(task?.remote_stage || task?.remoteStage || '').trim(); + const failureStage = String(task?.failure_stage || task?.failureStage || '').trim(); + const detail = String( + task?.error_message + || task?.errorMessage + || task?.failure_detail + || task?.failureDetail + || task?.last_input_error + || task?.lastInputError + || task?.status_text + || task?.statusText + || task?.message + || task?.detail + || task?.data?.detail + || '' + ).trim(); + if (detail) { + return failureStage && !detail.includes(failureStage) + ? `${detail}(${failureStage})` + : detail; + } + if (/api_otp_timeout/i.test(remoteStage)) { + return 'GPC OTP 超时,请重新创建任务'; + } + if (/api_pin_timeout/i.test(remoteStage)) { + return 'GPC PIN 超时,请重新创建任务'; + } + if (failureStage) { + return `GPC 任务失败:${failureStage}`; + } + return `任务状态 ${status || '未知'}`; + } + + async function stopGpcTaskBestEffort(apiUrl, taskId, apiKey, reason = '') { + if (!apiUrl || !taskId || !apiKey) { + return; + } + try { + const task = await postGpcTaskAction(apiUrl, taskId, 'stop', {}, apiKey, 15000); + const statusText = task?.status_text || task?.status || '已停止'; + await addLog(`步骤 7:已请求停止 GPC 任务(${statusText})。`, 'warn'); + } catch (error) { + await addLog(`步骤 7:停止 GPC 任务失败${reason ? `(${reason})` : ''}:${error?.message || String(error || '未知错误')}`, 'warn'); + } + } + + async function clearGpcTaskRuntimeState() { + const updates = { + plusManualConfirmationPending: false, + plusManualConfirmationRequestId: '', + plusManualConfirmationStep: 0, + plusManualConfirmationMethod: '', + plusManualConfirmationTitle: '', + plusManualConfirmationMessage: '', + gopayHelperTaskId: '', + gopayHelperTaskStatus: '', + gopayHelperStatusText: '', + gopayHelperRemoteStage: '', + gopayHelperApiWaitingFor: '', + gopayHelperApiInputDeadlineAt: '', + gopayHelperApiInputWaitSeconds: 0, + gopayHelperLastInputError: '', + gopayHelperOtpInvalidCount: 0, + gopayHelperFailureStage: '', + gopayHelperFailureDetail: '', + gopayHelperTaskPayload: null, + gopayHelperPinPayload: null, + gopayHelperResolvedOtp: '', + gopayHelperOtpRequestId: '', + gopayHelperOtpReferenceId: '', + }; + await setState(updates); + if (typeof broadcastDataUpdate === 'function') { + broadcastDataUpdate(updates); + } + } + + function isGpcTaskEndedError(error) { + return /^GPC_TASK_ENDED::/i.test(error?.message || String(error || '')); + } + + async function resolveGpcTaskOtp(state = {}, taskId = '', options = {}) { + let otp = ''; + const useLocalSmsHelper = Boolean(state?.gopayHelperLocalSmsHelperEnabled); + const retryCount = Math.max(0, Number(options?.retryCount) || 0); + const helperAfterMs = normalizeEpochMilliseconds(options?.afterMs || options?.after_ms || 0); + const lastInputError = String(options?.lastInputError || options?.last_input_error || '').trim(); + if (useLocalSmsHelper) { + try { + await addLog( + retryCount > 0 || lastInputError + ? `步骤 7:${lastInputError || 'OTP 校验未通过'},正在从本地 OTP Helper 等待新的 GPC OTP...` + : '步骤 7:正在从本地 OTP Helper 等待 GPC OTP...', + 'info' + ); + otp = await pollLocalSmsHelperOtp(state, taskId, { + afterMs: helperAfterMs, + singleAttempt: true, + requestTimeoutMs: 2000, + timeoutSeconds: 2, + }); + await addLog('步骤 7:本地 OTP Helper 已读取到 GPC OTP,准备提交验证。', 'ok'); + } catch (error) { + await addLog(`步骤 7:本地 OTP Helper 暂未读取到新 OTP:${error?.message || String(error || '未知错误')},继续等待远端任务状态更新。`, 'warn'); + } + } + if (otp) { + return otp; + } + if (useLocalSmsHelper) { + return ''; + } + await addLog('步骤 7:等待用户输入 OTP...', 'info'); + return requestGpcOtpInput({ + title: 'GPC OTP 验证', + message: retryCount > 0 || lastInputError + ? `${lastInputError || '上一次 OTP 校验未通过'},请重新输入正确的 OTP 验证码(task_id: ${taskId})` + : `请输入收到的 OTP 验证码(task_id: ${taskId})`, + lastInputError, + inputDeadlineAt: options?.inputDeadlineAt || options?.input_deadline_at || '', + taskId, + }); + } + async function executeGpcHelperBilling(state = {}) { - const referenceId = String(state?.gopayHelperReferenceId || '').trim(); + const taskId = String(state?.gopayHelperTaskId || '').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。'); + const apiKey = String( + state?.gopayHelperApiKey + || state?.gpcApiKey + || state?.apiKey + || '' + ).trim(); + const deadline = Date.now() + Math.max(120, Math.min(1200, Number(state?.gopayHelperTaskTimeoutSeconds) || 900)) * 1000; + let otpSubmitCount = 0; + let otpLastSubmittedAt = 0; + let lastSubmittedOtp = ''; + let pinSubmitted = false; + let terminalReached = false; + + if (!taskId) { + throw new Error('步骤 7:GPC 模式缺少 task_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'); - let otp = ''; - const useLocalSmsHelper = Boolean(state?.gopayHelperLocalSmsHelperEnabled); - if (useLocalSmsHelper) { - try { - await addLog('步骤 7:正在从本地 SMS Helper 等待 GPC OTP...', 'info'); - otp = await pollLocalSmsHelperOtp(state, referenceId); - await addLog('步骤 7:本地 SMS Helper 已读取到 GPC OTP,准备提交验证。', 'ok'); - } catch (error) { - await addLog(`步骤 7:本地 SMS Helper 未能自动读取 OTP:${error?.message || String(error || '未知错误')},改为手动输入。`, 'warn'); - } - } - if (!otp) { - await addLog('步骤 7:等待用户输入 OTP...', 'info'); - otp = await requestGpcOtpInput({ - title: 'GPC OTP 验证', - message: `请输入收到的 OTP 验证码(reference_id: ${referenceId})`, - referenceId, - }); + if (!apiKey) { + throw new Error('步骤 7:GPC 模式缺少 API Key。'); } - 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, ''); + const rawPin = String(state?.gopayHelperPin || '').trim(); + const pinDigits = rawPin.replace(/[^\d]/g, ''); + const pin = normalizeSixDigitPin(rawPin); if (!pin) { - throw new Error('步骤 7:GPC 模式缺少 PIN 配置。'); + if (taskId && apiUrl && apiKey) { + await stopGpcTaskBestEffort(apiUrl, taskId, apiKey, 'PIN 配置错误'); + } + throw new Error(pinDigits + ? '步骤 7:GPC PIN 必须是 6 位数字,请检查侧边栏配置。' + : '步骤 7:GPC 模式缺少 PIN 配置。'); } - await setState({ - gopayHelperChallengeId: challengeId, - gopayHelperFlowId: nextFlowId, - gopayHelperGoPayGuid: gopayGuid, - gopayHelperRedirectUrl: redirectUrl, - }); + await addLog(`步骤 7:GPC 模式开始轮询任务(task_id: ${taskId})...`, 'info'); + try { + while (Date.now() <= deadline) { + throwIfStopped(); + const task = await fetchGpcTaskStatus(apiUrl, taskId, apiKey); + const statusText = task?.status_text || task?.status || '处理中'; + const remoteStage = task?.remote_stage || ''; + const waitingFor = task?.api_waiting_for || ''; + await addLog(`步骤 7:GPC 任务状态:${statusText}${remoteStage ? `(${remoteStage})` : ''}${waitingFor ? `,等待 ${waitingFor.toUpperCase()}` : ''}`, 'info'); - 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)}`); + if (task.status === 'completed') { + terminalReached = true; + await setState({ + plusCheckoutSource: PLUS_PAYMENT_METHOD_GPC_HELPER, + }); + await addLog('步骤 7:GPC 任务已完成,准备继续下一步。', 'ok'); + await completeStepFromBackground(7, { + plusCheckoutSource: PLUS_PAYMENT_METHOD_GPC_HELPER, + }); + return; + } + + if (['failed', 'expired', 'discarded'].includes(task.status)) { + terminalReached = true; + throw buildGpcTaskEndedError(task, 'GPC 任务已结束,请重新创建任务。'); + } + + if (isGpcTaskOtpWait(task)) { + if (isGpcTaskInputDeadlineExpired(task)) { + throw buildGpcInputDeadlineError(task, 'OTP'); + } + if (task.last_input_error) { + await addLog( + `步骤 7:${task.last_input_error}${task.otp_invalid_count ? `(OTP 错误 ${task.otp_invalid_count} 次)` : ''}`, + 'warn' + ); + } + let otp = ''; + try { + otp = await resolveGpcTaskOtp(state, taskId, { + retryCount: otpSubmitCount, + afterMs: otpLastSubmittedAt, + lastInputError: task.last_input_error, + inputDeadlineAt: task.api_input_deadline_at, + }); + } catch (error) { + if (/OTP\s*输入(?:已取消|超时)/i.test(error?.message || String(error || ''))) { + throw new Error(`GPC_TASK_ENDED::${error?.message || 'OTP 输入已取消'},已结束当前 GPC 任务。`); + } + throw error; + } + if (!otp) { + await sleepWithStop(GPC_TASK_POLL_INTERVAL_MS); + continue; + } + const normalizedOtp = normalizeSixDigitOtp(otp); + if (!normalizedOtp) { + await addLog('步骤 7:OTP 必须是 6 位数字,等待重新输入。', 'warn'); + await sleepWithStop(GPC_TASK_POLL_INTERVAL_MS); + continue; + } + if (task.last_input_error && lastSubmittedOtp && normalizedOtp === lastSubmittedOtp) { + await addLog('步骤 7:本地 OTP Helper 返回的仍是上次已失败 OTP,等待新的验证码。', 'warn'); + await sleepWithStop(GPC_TASK_POLL_INTERVAL_MS); + continue; + } + await addLog('步骤 7:正在提交 OTP...', 'info'); + try { + await postGpcTaskAction( + apiUrl, + taskId, + 'otp', + buildGpcTaskOtpPayload({ otp: normalizedOtp }), + apiKey, + 30000 + ); + } catch (error) { + if (isGpcOtpFormatConflict(error)) { + await addLog(`步骤 7:OTP 提交被拒绝:${error?.message || String(error || 'OTP 格式错误')},等待重新输入。`, 'warn'); + await sleepWithStop(GPC_TASK_POLL_INTERVAL_MS); + continue; + } + throw error; + } + otpSubmitCount += 1; + otpLastSubmittedAt = Date.now(); + lastSubmittedOtp = normalizedOtp; + await addLog('步骤 7:OTP 已提交,继续等待 GPC 任务状态更新。', 'ok'); + } else if (isGpcTaskPinWait(task) && !pinSubmitted) { + if (isGpcTaskInputDeadlineExpired(task)) { + throw buildGpcInputDeadlineError(task, 'PIN'); + } + await addLog('步骤 7:正在提交 PIN...', 'info'); + let pinTask = null; + try { + pinTask = await postGpcTaskAction( + apiUrl, + taskId, + 'pin', + buildGpcTaskPinPayload({ pin }), + apiKey, + 30000 + ); + } catch (error) { + throw new Error(`GPC_TASK_ENDED::${error?.message || String(error || 'PIN 提交失败,请重新创建任务。')}`); + } + pinSubmitted = true; + await setState({ + gopayHelperPinPayload: pinTask, + }); + await addLog('步骤 7:PIN 已提交,继续轮询直到任务完成。', 'ok'); + } + + await sleepWithStop(GPC_TASK_POLL_INTERVAL_MS); + } + throw new Error('步骤 7:GPC 任务轮询超时。'); + } catch (error) { + if (!terminalReached) { + await stopGpcTaskBestEffort(apiUrl, taskId, apiKey, error?.message || '流程中断'); + } + if (isGpcTaskEndedError(error)) { + await clearGpcTaskRuntimeState(); + } + throw error; } - - 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 = '') { diff --git a/gopay-utils.js b/gopay-utils.js index 53eb4c1..eac491b 100644 --- a/gopay-utils.js +++ b/gopay-utils.js @@ -4,7 +4,7 @@ const PLUS_PAYMENT_METHOD_PAYPAL = 'paypal'; const PLUS_PAYMENT_METHOD_GOPAY = 'gopay'; const PLUS_PAYMENT_METHOD_GPC_HELPER = 'gpc-helper'; - const DEFAULT_GPC_HELPER_API_URL = 'https://gopay.hwork.pro'; + const DEFAULT_GPC_HELPER_API_URL = 'https://gpc.leftcode.xyz'; function normalizePlusPaymentMethod(value = '') { const normalized = String(value || '').trim().toLowerCase(); @@ -62,7 +62,10 @@ normalized = normalized.replace(/\/+$/g, ''); normalized = normalized.replace(/\/api\/checkout\/start$/i, ''); normalized = normalized.replace(/\/api\/gopay\/(?:otp|pin)$/i, ''); + normalized = normalized.replace(/\/api\/gp\/tasks(?:\/[^/?#]+)?(?:\/(?:otp|pin|stop))?(?:\?.*)?$/i, ''); + normalized = normalized.replace(/\/api\/gp\/balance(?:\?.*)?$/i, ''); normalized = normalized.replace(/\/api\/card\/balance(?:\?.*)?$/i, ''); + normalized = normalized.replace(/\/api\/card\/redeem-api-key(?:\?.*)?$/i, ''); return normalized || DEFAULT_GPC_HELPER_API_URL; } @@ -75,12 +78,85 @@ return `${baseUrl}${normalizedPath}`; } - function buildGpcCardBalanceUrl(apiUrl = '', cardKey = '') { - const endpoint = buildGpcHelperApiUrl(apiUrl, '/api/card/balance'); - if (!endpoint) { + function buildGpcApiKeyBalanceUrl(apiUrl = '') { + return buildGpcHelperApiUrl(apiUrl, '/api/gp/balance'); + } + + function buildGpcCardBalanceUrl(apiUrl = '') { + return buildGpcApiKeyBalanceUrl(apiUrl); + } + + function buildGpcApiKeyHeaders(apiKey = '', extraHeaders = {}) { + const headers = { + ...(extraHeaders && typeof extraHeaders === 'object' ? extraHeaders : {}), + }; + const normalizedApiKey = String(apiKey || '').trim(); + if (normalizedApiKey) { + headers['X-API-Key'] = normalizedApiKey; + } + return headers; + } + + function buildGpcTaskCreateUrl(apiUrl = '') { + return buildGpcHelperApiUrl(apiUrl, '/api/gp/tasks'); + } + + function normalizeGpcTaskId(value = '') { + return String(value || '').trim(); + } + + function buildGpcTaskQueryUrl(apiUrl = '', taskId = '') { + const normalizedTaskId = normalizeGpcTaskId(taskId); + return buildGpcHelperApiUrl(apiUrl, `/api/gp/tasks/${encodeURIComponent(normalizedTaskId)}`); + } + + function buildGpcTaskActionUrl(apiUrl = '', taskId = '', action = '') { + const normalizedTaskId = normalizeGpcTaskId(taskId); + const normalizedAction = String(action || '').trim().replace(/^\/+|\/+$/g, ''); + return buildGpcHelperApiUrl(apiUrl, `/api/gp/tasks/${encodeURIComponent(normalizedTaskId)}/${normalizedAction}`); + } + + function unwrapGpcResponse(payload = {}) { + if (!payload || typeof payload !== 'object' || Array.isArray(payload)) { + return payload; + } + const hasUnifiedShape = Object.prototype.hasOwnProperty.call(payload, 'data') + && ( + Object.prototype.hasOwnProperty.call(payload, 'code') + || Object.prototype.hasOwnProperty.call(payload, 'message') + ); + return hasUnifiedShape ? (payload.data ?? {}) : payload; + } + + function isGpcUnifiedResponseOk(payload = {}) { + if (!payload || typeof payload !== 'object' || Array.isArray(payload)) { + return true; + } + if (!Object.prototype.hasOwnProperty.call(payload, 'code')) { + return payload.ok !== false; + } + const code = Number(payload.code); + if (Number.isFinite(code)) { + return code >= 200 && code < 300; + } + return String(payload.code || '').trim() === '200'; + } + + function formatGpcErrorField(field) { + if (field === undefined || field === null) { return ''; } - return `${endpoint}?card_key=${encodeURIComponent(String(cardKey || '').trim())}`; + if (typeof field === 'string') { + return field.trim(); + } + if (typeof field !== 'object') { + return String(field).trim(); + } + const key = Array.isArray(field.loc) + ? field.loc.join('.') + : String(field.field || field.path || field.name || field.param || '').trim(); + const message = String(field.msg || field.message || field.error || field.detail || field.reason || '').trim(); + return [key, message].filter(Boolean).join(': ') || JSON.stringify(field); } function extractGpcResponseErrorDetail(payload = {}, status = 0) { @@ -93,6 +169,27 @@ return 'GOPAY已经绑了订阅,需要手动解绑'; } + const data = payload.data; + if (data && typeof data === 'object' && !Array.isArray(data)) { + const nestedDetail = data.detail ?? data.error ?? data.reason; + if (nestedDetail !== undefined && nestedDetail !== null && String(nestedDetail).trim()) { + const nestedText = String(nestedDetail).trim(); + return /account\s+already\s+linked/i.test(nestedText) + ? 'GOPAY已经绑了订阅,需要手动解绑' + : nestedText; + } + const fields = data.fields ?? data.errors; + if (Array.isArray(fields) && fields.length > 0) { + const formatted = fields + .map(formatGpcErrorField) + .filter(Boolean) + .join('; '); + if (formatted) { + return formatted; + } + } + } + const direct = payload.detail ?? payload.message ?? payload.error @@ -136,7 +233,6 @@ 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(); @@ -158,7 +254,6 @@ 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(); @@ -172,25 +267,45 @@ return { ...payload, challengeId: payload.challenge_id }; } + function buildGpcTaskOtpPayload(input = {}) { + return { + otp: normalizeGoPayOtp(input.otp ?? input.code ?? ''), + }; + } + + function buildGpcTaskPinPayload(input = {}) { + return { + pin: normalizeGoPayPin(input.pin ?? ''), + }; + } + function formatGpcBalancePayload(payload = {}) { - if (!payload || typeof payload !== 'object') { + const data = unwrapGpcResponse(payload); + if (!data || typeof data !== 'object') { return ''; } const candidates = [ - payload.remaining_uses, - payload.remainingUses, - payload.balance, - payload.remaining, - payload.uses, - payload.available_uses, - payload.availableUses, + data.remaining_uses, + data.remainingUses, + data.balance, + data.remaining, + data.uses, + data.available_uses, + data.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 totalUses = data.total_uses ?? data.totalUses; + const usedUses = data.used_uses ?? data.usedUses; + const status = String(data.status || data.card_status || data.cardStatus || '').trim(); + const flowId = String(data.flow_id || data.flowId || '').trim(); const parts = []; if (firstValue !== undefined) { - parts.push(`余额 ${firstValue}`); + parts.push(totalUses !== undefined && totalUses !== null && String(totalUses).trim() !== '' + ? `余额 ${firstValue}/${totalUses}` + : `余额 ${firstValue}`); + } + if (usedUses !== undefined && usedUses !== null && String(usedUses).trim() !== '') { + parts.push(`已用 ${usedUses}`); } if (status) { parts.push(`状态 ${status}`); @@ -208,14 +323,23 @@ PLUS_PAYMENT_METHOD_GOPAY, PLUS_PAYMENT_METHOD_PAYPAL, buildGpcCardBalanceUrl, + buildGpcApiKeyBalanceUrl, + buildGpcApiKeyHeaders, buildGpcHelperApiUrl, buildGpcOtpPayload, buildGpcOtpRetryPayload, buildGpcPinPayload, buildGpcPinRetryPayload, + buildGpcTaskActionUrl, + buildGpcTaskCreateUrl, + buildGpcTaskOtpPayload, + buildGpcTaskPinPayload, + buildGpcTaskQueryUrl, extractGpcResponseErrorDetail, formatGpcBalancePayload, + isGpcUnifiedResponseOk, normalizeGpcHelperBaseUrl, + normalizeGpcTaskId, normalizeGoPayCountryCode, normalizeGoPayPhone, normalizeGoPayPhoneForCountry, @@ -223,5 +347,6 @@ normalizeGoPayPin, normalizeGpcOtpChannel, normalizePlusPaymentMethod, + unwrapGpcResponse, }; }); diff --git a/sidepanel/sidepanel.html b/sidepanel/sidepanel.html index afe062d..e19cd84 100644 --- a/sidepanel/sidepanel.html +++ b/sidepanel/sidepanel.html @@ -264,7 +264,7 @@ - + PayPal 订阅链路 @@ -283,15 +283,20 @@ + @@ -285,8 +285,11 @@
- 延迟 -
-
+ 第六步 +
+ +
+ 延迟
-
- 步间间隔 -
- - -
-
@@ -594,6 +586,30 @@
+
+ 步间间隔 +
+ + +
+
+
+ +
+ 授权总超时 +
+
+ + 关闭后只取消 Step 7 后链总预算 +
线程间隔
@@ -604,20 +620,6 @@
-
- 授权总超时 -
- - 关闭后只取消 Step 7 后链总预算 -
-
OAuth 等待中...