From 56f52cb858498e2bb211dd5f9bf06233b390adc6 Mon Sep 17 00:00:00 2001 From: QLHazyCoder <2825305047@qq.com> Date: Fri, 29 May 2026 10:09:26 +0800 Subject: [PATCH] Refactor GPC plus flow to page recharge --- README.md | 2 +- background.js | 338 ++--- background/auto-run-controller.js | 22 +- background/logging-status.js | 2 +- background/message-router.js | 22 +- core/flow-kernel/logging-status.js | 2 +- core/flow-kernel/runtime-state.js | 29 +- docs/使用教程/使用教程.md | 4 +- docs/使用教程/使用教程书写模板.md | 2 +- .../分部分/09-GPC-卡密与-API-使用说明.md | 129 -- .../分部分/09-GPC-卡密页面充值说明.md | 66 + .../background/steps/create-plus-checkout.js | 533 +++----- .../background/steps/fill-plus-checkout.js | 1218 +++-------------- flows/openai/workflow.js | 20 +- gopay-utils.js | 193 +-- scripts/gpc_sms_helper_macos.py | 424 ------ sidepanel/sidepanel.css | 26 +- sidepanel/sidepanel.html | 94 +- sidepanel/sidepanel.js | 760 +++------- tests/auto-run-add-phone-stop.test.js | 14 +- tests/auto-run-kiro-flow-selection.test.js | 6 +- tests/auto-run-step6-restart.test.js | 26 +- ...ackground-account-history-settings.test.js | 50 +- .../background-logging-status-module.test.js | 4 +- ...ckground-message-router-step2-skip.test.js | 42 +- ...nd-settings-import-mode-validation.test.js | 4 +- tests/gopay-utils.test.js | 69 +- tests/gpc-sms-helper-script.test.js | 201 --- tests/plus-account-access-strategy.test.js | 11 - ...us-checkout-billing-tab-resolution.test.js | 957 ++----------- tests/plus-checkout-create-wait.test.js | 615 ++------- ...sidepanel-auto-run-content-refresh.test.js | 4 +- tests/sidepanel-contribution-mode.test.js | 1 + tests/sidepanel-icloud-provider.test.js | 1 + tests/sidepanel-mail2925-base-email.test.js | 1 + ...epanel-phone-verification-settings.test.js | 1 + tests/sidepanel-plus-payment-method.test.js | 521 +------ tests/step-definitions-module.test.js | 37 +- 项目完整链路说明.md | 10 +- 项目文件结构说明.md | 20 +- 40 files changed, 1278 insertions(+), 5203 deletions(-) delete mode 100644 docs/使用教程/分部分/09-GPC-卡密与-API-使用说明.md create mode 100644 docs/使用教程/分部分/09-GPC-卡密页面充值说明.md delete mode 100644 scripts/gpc_sms_helper_macos.py delete mode 100644 tests/gpc-sms-helper-script.test.js diff --git a/README.md b/README.md index e95a8d5..2ac0480 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,7 @@ ## 邮箱与验证码能力 - 支持网页邮箱轮询、API 邮箱轮询和本地 helper 读取三类模式。 -- 支持注册验证码、登录验证码、绑定邮箱验证码,以及 Plus / GPC 场景下的 OTP 处理。 +- 支持注册验证码、登录验证码、绑定邮箱验证码,以及 GoPay 场景下的 OTP / PIN 处理。 - `2925` 支持多账号池、自动登录、自动切号、24 小时冷却。 - `Hotmail` 支持远程服务模式和本地 helper 模式。 - `自定义邮箱池` 和 `自定义邮箱服务号池` 都可以和自动运行轮数联动。 diff --git a/background.js b/background.js index d2e57ea..0ecbd43 100644 --- a/background.js +++ b/background.js @@ -578,7 +578,7 @@ const SUB2API_STEP1_RESPONSE_TIMEOUT_MS = 90000; const SUB2API_STEP9_RESPONSE_TIMEOUT_MS = 120000; const DEFAULT_SUB2API_URL = ''; const DEFAULT_CODEX2API_URL = 'http://localhost:8080/admin/accounts'; -const DEFAULT_GPC_HELPER_API_URL = 'https://gpc.qlhazycoder.top'; +const DEFAULT_GPC_BASE_URL = 'https://gpc.qlhazycoder.top'; const DEFAULT_SUB2API_GROUP_NAME = 'codex'; const DEFAULT_SUB2API_PROXY_NAME = ''; const DEFAULT_SUB2API_ACCOUNT_PRIORITY = 1; @@ -861,11 +861,6 @@ function normalizePlusPaymentMethod(value = '') { return normalized === PLUS_PAYMENT_METHOD_GOPAY ? PLUS_PAYMENT_METHOD_GOPAY : PLUS_PAYMENT_METHOD_PAYPAL; } -function normalizeGpcHelperPhoneMode(value = '') { - const normalized = String(value || '').trim().toLowerCase(); - return normalized === 'auto' || normalized === 'builtin' ? 'auto' : 'manual'; -} - function normalizeOpenAiContributionSource(value = '') { const normalized = String(value || '').trim().toLowerCase(); return normalized === CONTRIBUTION_SOURCE_SUB2API @@ -1342,47 +1337,16 @@ const PERSISTED_SETTING_DEFAULTS = { gopayPhone: '', gopayOtp: '', gopayPin: '', - gopayHelperApiUrl: DEFAULT_GPC_HELPER_API_URL, - gopayHelperApiKey: '', - gopayHelperCardKey: '', - gopayHelperPhoneMode: 'manual', - gopayHelperPhoneNumber: '', - gopayHelperCountryCode: '+86', - gopayHelperPin: '', - gopayHelperOtpChannel: 'whatsapp', - gopayHelperLocalSmsHelperEnabled: false, - gopayHelperLocalSmsHelperUrl: 'http://127.0.0.1:18767', - gopayHelperLocalSmsTimeoutSeconds: 90, - gopayHelperLocalSmsPollIntervalSeconds: 2, - gopayHelperReferenceId: '', - gopayHelperGoPayGuid: '', - gopayHelperRedirectUrl: '', - gopayHelperNextAction: '', - gopayHelperFlowId: '', - gopayHelperChallengeId: '', - gopayHelperStartPayload: null, - gopayHelperTaskId: '', - gopayHelperTaskStatus: '', - gopayHelperStatusText: '', - gopayHelperRemoteStage: '', - gopayHelperApiWaitingFor: '', - gopayHelperApiInputDeadlineAt: '', - gopayHelperApiInputWaitSeconds: 0, - gopayHelperLastInputError: '', - gopayHelperOtpInvalidCount: 0, - gopayHelperFailureStage: '', - gopayHelperFailureDetail: '', - gopayHelperTaskPayload: null, - gopayHelperTaskProgressSignature: '', - gopayHelperTaskProgressAt: 0, - gopayHelperTaskProgressTaskId: '', - gopayHelperBalance: '', - gopayHelperBalancePayload: null, - gopayHelperBalanceUpdatedAt: 0, - gopayHelperBalanceError: '', - gopayHelperRemainingUses: 0, - gopayHelperAutoModeEnabled: false, - gopayHelperApiKeyStatus: '', + gpcBaseUrl: DEFAULT_GPC_BASE_URL, + gpcCardKey: '', + gpcBalance: '', + gpcBalancePayload: null, + gpcBalanceUpdatedAt: 0, + gpcBalanceError: '', + gpcRemainingUses: 0, + gpcCardStatus: '', + gpcPageStatus: '', + gpcPageStatusText: '', autoRunSkipFailures: false, autoRunFallbackThreadIntervalMinutes: 0, operationDelayEnabled: true, @@ -3396,98 +3360,42 @@ function normalizePersistentSettingValue(key, value) { return self.GoPayUtils?.normalizeGoPayPin ? self.GoPayUtils.normalizeGoPayPin(value) : String(value || ''); - case 'gopayHelperPhoneMode': - return self.GoPayUtils?.normalizeGpcHelperPhoneMode - ? self.GoPayUtils.normalizeGpcHelperPhoneMode(value) - : (String(value || '').trim().toLowerCase() === 'auto' || String(value || '').trim().toLowerCase() === 'builtin' ? 'auto' : 'manual'); - 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 'gopayHelperOtpChannel': - return self.GoPayUtils?.normalizeGpcOtpChannel - ? self.GoPayUtils.normalizeGpcOtpChannel(value) - : (String(value || '').trim().toLowerCase() === 'sms' ? 'sms' : 'whatsapp'); - case 'gopayHelperLocalSmsHelperUrl': - return normalizeLocalHttpBaseUrl( - value, - PERSISTED_SETTING_DEFAULTS.gopayHelperLocalSmsHelperUrl || 'http://127.0.0.1:18767' - ); - case 'gopayHelperLocalSmsTimeoutSeconds': - return normalizeBoundedIntegerSetting( - value, - PERSISTED_SETTING_DEFAULTS.gopayHelperLocalSmsTimeoutSeconds, - 10, - 300 - ); - case 'gopayHelperLocalSmsPollIntervalSeconds': - return normalizeBoundedIntegerSetting( - value, - PERSISTED_SETTING_DEFAULTS.gopayHelperLocalSmsPollIntervalSeconds, - 1, - 30 - ); - case 'gopayHelperApiUrl': + case 'gpcBaseUrl': { - const defaultGpcHelperApiUrl = PERSISTED_SETTING_DEFAULTS.gopayHelperApiUrl - || (typeof DEFAULT_GPC_HELPER_API_URL !== 'undefined' ? DEFAULT_GPC_HELPER_API_URL : 'https://gpc.qlhazycoder.top'); - const normalizedGpcHelperApiUrl = self.GoPayUtils?.normalizeGpcHelperBaseUrl - ? self.GoPayUtils.normalizeGpcHelperBaseUrl(value || defaultGpcHelperApiUrl) - : String(value || defaultGpcHelperApiUrl).trim().replace(/\/+$/g, ''); - if (!self.GoPayUtils?.normalizeGpcHelperBaseUrl) { + const defaultGpcBaseUrl = PERSISTED_SETTING_DEFAULTS.gpcBaseUrl + || (typeof DEFAULT_GPC_BASE_URL !== 'undefined' ? DEFAULT_GPC_BASE_URL : 'https://gpc.qlhazycoder.top'); + const normalizedGpcBaseUrl = self.GoPayUtils?.normalizeGpcBaseUrl + ? self.GoPayUtils.normalizeGpcBaseUrl(value || defaultGpcBaseUrl) + : String(value || defaultGpcBaseUrl).trim().replace(/\/+$/g, ''); + if (!self.GoPayUtils?.normalizeGpcBaseUrl) { try { - const parsed = new URL(normalizedGpcHelperApiUrl); + const parsed = new URL(normalizedGpcBaseUrl); const hostname = parsed.hostname.toLowerCase(); if (hostname !== 'gpc.qlhazycoder.top' && hostname !== 'localhost' && hostname !== '127.0.0.1') { - return defaultGpcHelperApiUrl; + return defaultGpcBaseUrl; } } catch { - return defaultGpcHelperApiUrl; + return defaultGpcBaseUrl; } } - return normalizedGpcHelperApiUrl; + return normalizedGpcBaseUrl; } - case 'gopayHelperApiKey': - case 'gopayHelperCardKey': - case 'gopayHelperReferenceId': - case 'gopayHelperGoPayGuid': - case 'gopayHelperRedirectUrl': - 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': - case 'gopayHelperApiKeyStatus': + case 'gpcCardKey': + return self.GoPayUtils?.normalizeGpcCardKey + ? self.GoPayUtils.normalizeGpcCardKey(value) + : String(value || '').trim().toUpperCase(); + case 'gpcBalance': + case 'gpcBalanceError': + case 'gpcCardStatus': + case 'gpcPageStatus': + case 'gpcPageStatusText': return String(value || '').trim(); - case 'gopayHelperBalancePayload': - case 'gopayHelperStartPayload': - case 'gopayHelperTaskPayload': + case 'gpcBalancePayload': return value && typeof value === 'object' && !Array.isArray(value) ? value : null; - case 'gopayHelperBalanceUpdatedAt': - case 'gopayHelperApiInputWaitSeconds': - case 'gopayHelperOtpInvalidCount': - case 'gopayHelperRemainingUses': + case 'gpcBalanceUpdatedAt': + case 'gpcRemainingUses': return Math.max(0, Number(value) || 0); case 'autoRunSkipFailures': - case 'gopayHelperLocalSmsHelperEnabled': - case 'gopayHelperAutoModeEnabled': return Boolean(value); case 'operationDelayEnabled': return true; @@ -9637,7 +9545,7 @@ function getErrorMessage(error) { return loggingStatus.getErrorMessage(error); } return String(typeof error === 'string' ? error : error?.message || '') - .replace(/^GPC_TASK_ENDED::/i, '') + .replace(/^GPC_PAGE_FLOW_ENDED::/i, '') .replace(/^AUTO_RUN_STEP_IDLE_RESTART::/i, ''); } @@ -9872,9 +9780,9 @@ function isPlusCheckoutNonFreeTrialFailure(error) { return /PLUS_CHECKOUT_NON_FREE_TRIAL::|今日应付金额不是\s*0|没有免费试用资格|该账号已经开通过\s*ChatGPT\s*订阅套餐,不能重复订阅(?:。)?(?:(\s*checkout_order\s*)|\(\s*checkout_order\s*\))?/i.test(message); } -function isGpcTaskEndedFailure(error) { +function isGpcPageFlowEndedFailure(error) { const message = String(typeof error === 'string' ? error : error?.message || ''); - return /GPC_TASK_ENDED::/i.test(message); + return /GPC_PAGE_FLOW_ENDED::/i.test(message); } function isGpcCheckoutRestartRequiredFailure(error) { @@ -9884,10 +9792,10 @@ function isGpcCheckoutRestartRequiredFailure(error) { if (/PLUS_CHECKOUT_NON_FREE_TRIAL::|今日应付金额不是\s*0|没有免费试用资格/i.test(combinedMessage)) { return false; } - if (/GPC_TASK_ENDED::/i.test(rawMessage)) { + if (/GPC_PAGE_FLOW_ENDED::/i.test(rawMessage)) { return true; } - return /GPC\s*API\s*请求超时|GPC\s*任务状态超过\s*\d+\s*秒无进展|GPC[\s\S]*请重新创建任务|步骤\s*[67][\s\S]*GPC[\s\S]*(?:access\s*token|accessToken|任务轮询超时|请求超时|超时|timeout|timed\s*out|卡死|无响应|失败)|account\s+already\s+linked|GOPAY已经绑了订阅|(?:账号|账户|GoPay|GOPAY)[\s\S]*(?:已绑定|已经绑定|已绑|绑了订阅|绑定了订阅)|创建\s*GPC\s*订单失败[\s\S]*(?:任务已结束|任务结束|failed|expired|discarded|请求超时|timeout|timed\s*out)/i.test(message); + return /GPC\s*页面[\s\S]*(?:请重新准备|请求超时|超时|timeout|timed\s*out|卡死|无响应|失败|未检测到订阅完成|已尝试启动)|步骤\s*[67][\s\S]*GPC[\s\S]*(?:页面|请求超时|超时|timeout|timed\s*out|卡死|无响应|失败)|account\s+already\s+linked|GOPAY已经绑了订阅|(?:账号|账户|GoPay|GOPAY)[\s\S]*(?:已绑定|已经绑定|已绑|绑了订阅|绑定了订阅)/i.test(message); } function isPlusCheckoutRestartStep(step, stepExecutionKey = '', state = {}) { @@ -10060,33 +9968,8 @@ function getDownstreamStateResets(step, state = {}) { plusManualConfirmationMethod: '', plusManualConfirmationTitle: '', plusManualConfirmationMessage: '', - gopayHelperReferenceId: '', - gopayHelperGoPayGuid: '', - gopayHelperRedirectUrl: '', - gopayHelperNextAction: '', - gopayHelperFlowId: '', - gopayHelperChallengeId: '', - gopayHelperStartPayload: null, - gopayHelperTaskId: '', - gopayHelperTaskStatus: '', - gopayHelperStatusText: '', - gopayHelperRemoteStage: '', - gopayHelperApiWaitingFor: '', - gopayHelperApiInputDeadlineAt: '', - gopayHelperApiInputWaitSeconds: 0, - gopayHelperLastInputError: '', - gopayHelperOtpInvalidCount: 0, - gopayHelperFailureStage: '', - gopayHelperFailureDetail: '', - gopayHelperTaskPayload: null, - gopayHelperOrderCreatedAt: 0, - gopayHelperTaskProgressSignature: '', - gopayHelperTaskProgressAt: 0, - gopayHelperTaskProgressTaskId: '', - gopayHelperPinPayload: null, - gopayHelperResolvedOtp: '', - gopayHelperOtpRequestId: '', - gopayHelperOtpReferenceId: '', + gpcPageStatus: '', + gpcPageStatusText: '', }; if (step <= 1) { @@ -10185,10 +10068,8 @@ function getDownstreamStateResets(step, state = {}) { plusManualConfirmationMethod: '', plusManualConfirmationTitle: '', plusManualConfirmationMessage: '', - gopayHelperResolvedOtp: '', - gopayHelperLastInputError: '', - gopayHelperOtpRequestId: '', - gopayHelperOtpReferenceId: '', + gpcPageStatus: '', + gpcPageStatusText: '', } : {}), ...(isApprovalNode ? { plusPaypalApprovedAt: null, @@ -12581,49 +12462,44 @@ async function maybeSwitchIpProxyAfterAutoRunRoundSuccess(payload = {}) { return switchResult; } -function resolveGpcHelperBaseUrl(apiUrl = '') { - if (self.GoPayUtils?.normalizeGpcHelperBaseUrl) { - return self.GoPayUtils.normalizeGpcHelperBaseUrl(apiUrl || DEFAULT_GPC_HELPER_API_URL); +function resolveGpcBaseUrl(apiUrl = '') { + if (self.GoPayUtils?.normalizeGpcBaseUrl) { + return self.GoPayUtils.normalizeGpcBaseUrl(apiUrl || DEFAULT_GPC_BASE_URL); } - let normalized = String(apiUrl || DEFAULT_GPC_HELPER_API_URL).trim().replace(/\/+$/g, ''); + let normalized = String(apiUrl || DEFAULT_GPC_BASE_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\/web\/card\/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; + return normalized || DEFAULT_GPC_BASE_URL; } -function buildGpcApiKeyBalanceRequestUrl(apiUrl = '') { - if (self.GoPayUtils?.buildGpcApiKeyBalanceUrl) { - return self.GoPayUtils.buildGpcApiKeyBalanceUrl(apiUrl); +function normalizeGpcCardKey(value = '') { + if (self.GoPayUtils?.normalizeGpcCardKey) { + return self.GoPayUtils.normalizeGpcCardKey(value); } + return String(value || '').trim().toUpperCase(); +} + +function isGpcCardKeyFormat(value = '') { + if (self.GoPayUtils?.isGpcCardKeyFormat) { + return self.GoPayUtils.isGpcCardKeyFormat(value); + } + return /^GPC-[A-F0-9]{8}-[A-F0-9]{8}-[A-F0-9]{8}$/.test(normalizeGpcCardKey(value)); +} + +function buildGpcCardBalanceRequestUrl(apiUrl = '', cardKey = '') { if (self.GoPayUtils?.buildGpcCardBalanceUrl) { - return self.GoPayUtils.buildGpcCardBalanceUrl(apiUrl); + return self.GoPayUtils.buildGpcCardBalanceUrl(apiUrl, cardKey); } - const baseUrl = resolveGpcHelperBaseUrl(apiUrl); + const baseUrl = resolveGpcBaseUrl(apiUrl); if (!baseUrl) { return ''; } - return `${baseUrl}/api/gp/balance`; + const normalizedCardKey = normalizeGpcCardKey(cardKey); + return `${baseUrl}/api/web/card/balance${normalizedCardKey ? `?card_key=${encodeURIComponent(normalizedCardKey)}` : ''}`; } -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 = {}) { +function formatGpcCardBalancePayload(payload = {}) { if (self.GoPayUtils?.formatGpcBalancePayload) { return self.GoPayUtils.formatGpcBalancePayload(payload); } @@ -12643,28 +12519,26 @@ function formatGpcApiKeyBalancePayload(payload = {}) { ].filter(Boolean).join(','); } -async function refreshGpcApiKeyBalance(state = {}, options = {}) { - const apiUrl = resolveGpcHelperBaseUrl(state?.gopayHelperApiUrl || DEFAULT_GPC_HELPER_API_URL); - const apiKey = String( - state?.gopayHelperApiKey - || state?.gpcApiKey - || state?.apiKey - || '' - ).trim(); +async function refreshGpcCardBalance(state = {}, options = {}) { + const apiUrl = resolveGpcBaseUrl(state?.gpcBaseUrl || DEFAULT_GPC_BASE_URL); + const cardKey = normalizeGpcCardKey(state?.gpcCardKey || state?.cardKey || ''); if (!apiUrl) { - throw new Error('缺少 GPC API 地址。'); + throw new Error('缺少 GPC 页面地址。'); } - if (!apiKey) { - throw new Error('缺少 GPC API Key。'); + if (!cardKey) { + throw new Error('缺少 GPC 卡密。'); } - const requestUrl = buildGpcApiKeyBalanceRequestUrl(apiUrl); + if (!isGpcCardKeyFormat(cardKey)) { + throw new Error('GPC 卡密格式不正确,应类似 GPC-6C9F1A32-45734795-914E6F00。'); + } + const requestUrl = buildGpcCardBalanceRequestUrl(apiUrl, cardKey); if (!requestUrl) { - throw new Error('缺少 GPC API 地址。'); + throw new Error('缺少 GPC 卡密查询接口。'); } const response = await fetch(requestUrl, { method: 'GET', - headers: buildGpcApiKeyHeaders(apiKey, { Accept: 'application/json' }), + headers: { Accept: 'application/json' }, }); const rawText = await response.text(); let payload = {}; @@ -12682,26 +12556,19 @@ async function refreshGpcApiKeyBalance(state = {}, options = {}) { const remainingUses = self.GoPayUtils?.getGpcBalanceRemainingUses ? self.GoPayUtils.getGpcBalanceRemainingUses(balanceData) : Math.max(0, Number(balanceData.remaining_uses ?? balanceData.remainingUses ?? balanceData.balance ?? balanceData.remaining) || 0); - const autoModeEnabled = self.GoPayUtils?.isGpcAutoModeEnabled - ? self.GoPayUtils.isGpcAutoModeEnabled(balanceData) - : Boolean(balanceData.auto_mode_enabled ?? balanceData.autoModeEnabled); - const apiKeyStatus = self.GoPayUtils?.getGpcApiKeyStatus - ? self.GoPayUtils.getGpcApiKeyStatus(balanceData) + const cardStatus = self.GoPayUtils?.getGpcCardStatus + ? self.GoPayUtils.getGpcCardStatus(balanceData) : String(balanceData.status || balanceData.card_status || balanceData.cardStatus || '').trim(); - const balanceText = formatGpcApiKeyBalancePayload(payload) || rawText || '未知'; + const balanceText = formatGpcCardBalancePayload(payload) || rawText || '未知'; const updates = { - gopayHelperBalance: balanceText, - gopayHelperBalancePayload: Object.keys(balanceData).length > 0 ? balanceData : { raw: String(balancePayload || '') }, - gopayHelperBalanceUpdatedAt: Date.now(), - gopayHelperBalanceError: '', - gopayHelperRemainingUses: Math.max(0, Number(remainingUses) || 0), - gopayHelperAutoModeEnabled: Boolean(autoModeEnabled), - gopayHelperApiKeyStatus: apiKeyStatus, + gpcCardKey: cardKey, + gpcBalance: balanceText, + gpcBalancePayload: Object.keys(balanceData).length > 0 ? balanceData : { raw: String(balancePayload || '') }, + gpcBalanceUpdatedAt: Date.now(), + gpcBalanceError: '', + gpcRemainingUses: Math.max(0, Number(remainingUses) || 0), + gpcCardStatus: cardStatus, }; - const flowId = String(balancePayload?.flow_id || balancePayload?.flowId || '').trim(); - if (flowId) { - updates.gopayHelperFlowId = flowId; - } const unifiedOk = self.GoPayUtils?.isGpcUnifiedResponseOk ? self.GoPayUtils.isGpcUnifiedResponseOk(payload) @@ -12710,10 +12577,10 @@ async function refreshGpcApiKeyBalance(state = {}, options = {}) { 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 || '余额查询失败') }; + const errorUpdates = { ...updates, gpcBalanceError: String(detail || 'GPC 卡密查询失败') }; await setPersistentSettings(errorUpdates); broadcastDataUpdate(errorUpdates); - throw new Error(String(detail || '余额查询失败')); + throw new Error(String(detail || 'GPC 卡密查询失败')); } await setPersistentSettings(updates); @@ -12721,23 +12588,20 @@ async function refreshGpcApiKeyBalance(state = {}, options = {}) { const reason = String(options?.reason || '').trim(); await addLog( reason === 'round_success' - ? `GPC 余额已更新:${balanceText}` - : `GPC 余额查询成功:${balanceText}`, + ? `GPC 卡密剩余次数已更新:${balanceText}` + : `GPC 卡密查询成功:${balanceText}`, 'info' ); return { balance: balanceText, payload, - data: updates.gopayHelperBalancePayload, - remainingUses: updates.gopayHelperRemainingUses, - autoModeEnabled: updates.gopayHelperAutoModeEnabled, - apiKeyStatus: updates.gopayHelperApiKeyStatus, - updatedAt: updates.gopayHelperBalanceUpdatedAt, + data: updates.gpcBalancePayload, + remainingUses: updates.gpcRemainingUses, + cardStatus: updates.gpcCardStatus, + updatedAt: updates.gpcBalanceUpdatedAt, }; } -const refreshGpcCardBalance = refreshGpcApiKeyBalance; - const autoRunController = self.MultiPageBackgroundAutoRunController?.createAutoRunController({ addLog, appendAccountRunRecord: (...args) => appendAndBroadcastAccountRunRecord(...args), @@ -12764,7 +12628,7 @@ const autoRunController = self.MultiPageBackgroundAutoRunController?.createAutoR isAddPhoneAuthFailure, isPhoneSmsPlatformRateLimitFailure, isPlusCheckoutNonFreeTrialFailure, - isGpcTaskEndedFailure, + isGpcPageFlowEndedFailure, isKiroProxyFailure, isRestartCurrentAttemptError, isStep4Route405RecoveryLimitFailure, @@ -13538,10 +13402,10 @@ async function runAutoSequenceFromNodeGraph(startNodeId, context = {}) { ? gpcCheckoutRestartCount : (isGoPayCheckoutStep ? goPayCheckoutRestartCount : plusCheckoutRestartCount); const checkoutLabel = isGpcCheckoutStep - ? 'GPC 任务' + ? 'GPC 页面流程' : (isGoPayCheckoutStep ? 'GoPay 订阅' : 'Plus Checkout'); const recreateLabel = isGpcCheckoutStep - ? '重新创建 GPC 任务' + ? '重新准备 GPC 页面' : (isGoPayCheckoutStep ? '重新创建 GoPay 订阅' : '重新创建 Plus Checkout'); await addLog( `节点 ${getNodeLabel(nodeId, latestState)}:检测到 ${checkoutLabel} 失败/卡住,准备回到节点 plus-checkout-create ${recreateLabel}(第 ${checkoutRestartCount} 次)。原因:${getErrorMessage(err)}`, diff --git a/background/auto-run-controller.js b/background/auto-run-controller.js index d75e784..c5ff064 100644 --- a/background/auto-run-controller.js +++ b/background/auto-run-controller.js @@ -25,7 +25,7 @@ getState, hasSavedNodeProgress, isAddPhoneAuthFailure, - isGpcTaskEndedFailure, + isGpcPageFlowEndedFailure, isKiroProxyFailure, isPhoneSmsPlatformRateLimitFailure, isPlusCheckoutNonFreeTrialFailure, @@ -806,9 +806,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 blockedByGpcPageFlowEnded = typeof isGpcPageFlowEndedFailure === 'function' + ? isGpcPageFlowEndedFailure(err) + : /GPC_PAGE_FLOW_ENDED::/i.test(err?.message || String(err || '')); const blockedBySignupUserAlreadyExists = typeof isSignupUserAlreadyExistsFailure === 'function' && !keepSameEmailUntilAddPhone && isSignupUserAlreadyExistsFailure(err); @@ -819,7 +819,7 @@ const canRetry = !blockedByAddPhone && !blockedByPhoneNoSupply && !blockedByPlusNonFreeTrial - && !blockedByGpcTaskEnded + && !blockedByGpcPageFlowEnded && !blockedBySignupUserAlreadyExists && !blockedByStep4Route405 && !blockedByKiroProxy @@ -935,18 +935,18 @@ break; } - if (blockedByGpcTaskEnded) { + if (blockedByGpcPageFlowEnded) { roundSummary.status = 'failed'; roundSummary.finalFailureReason = reason; await setState({ autoRunRoundSummaries: serializeAutoRunRoundSummaries(totalRuns, roundSummaries), }); await appendRoundRecordIfNeeded('failed', reason, err); - cancelPendingCommands('当前轮因 GPC 任务已结束。'); + cancelPendingCommands('当前轮因 GPC 页面流程已结束。'); await broadcastStopToContentScripts(); if (!autoRunSkipFailures) { await addLog( - `第 ${targetRun}/${totalRuns} 轮 GPC 任务已结束,自动重试未开启,当前自动运行将停止。`, + `第 ${targetRun}/${totalRuns} 轮 GPC 页面流程已结束,自动重试未开启,当前自动运行将停止。`, 'warn' ); stoppedEarly = true; @@ -959,11 +959,11 @@ break; } - await addLog(`第 ${targetRun}/${totalRuns} 轮 GPC 任务已结束,本轮将直接失败并跳过剩余重试。`, 'warn'); + await addLog(`第 ${targetRun}/${totalRuns} 轮 GPC 页面流程已结束,本轮将直接失败并跳过剩余重试。`, 'warn'); await addLog( targetRun < totalRuns - ? `第 ${targetRun}/${totalRuns} 轮因 GPC 任务结束提前结束,自动流程将继续下一轮。` - : `第 ${targetRun}/${totalRuns} 轮因 GPC 任务结束提前结束,已无后续轮次,本次自动运行结束。`, + ? `第 ${targetRun}/${totalRuns} 轮因 GPC 页面流程结束提前结束,自动流程将继续下一轮。` + : `第 ${targetRun}/${totalRuns} 轮因 GPC 页面流程结束提前结束,已无后续轮次,本次自动运行结束。`, 'warn' ); forceFreshTabsNextRun = true; diff --git a/background/logging-status.js b/background/logging-status.js index d351908..519f08a 100644 --- a/background/logging-status.js +++ b/background/logging-status.js @@ -92,7 +92,7 @@ function getErrorMessage(error) { return String(typeof error === 'string' ? error : error?.message || '') - .replace(/^GPC_TASK_ENDED::/i, '') + .replace(/^GPC_PAGE_FLOW_ENDED::/i, '') .replace(/^AUTO_RUN_STEP_IDLE_RESTART::/i, ''); } diff --git a/background/message-router.js b/background/message-router.js index 45b7ed8..3fa9d93 100644 --- a/background/message-router.js +++ b/background/message-router.js @@ -1181,7 +1181,6 @@ 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 }; } @@ -1198,23 +1197,6 @@ 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); @@ -1232,7 +1214,7 @@ const cancelMessage = method === 'gopay' ? '已取消 GoPay 订阅确认' - : (isGpcOtp ? '已取消 GPC OTP 输入' : '已取消当前手动确认'); + : '已取消当前手动确认'; await setNodeStatus(confirmationNodeId, 'failed'); await addLog(`步骤 ${step}:${cancelMessage}。`, 'warn'); await appendManualAccountRunRecordIfNeeded( @@ -1688,7 +1670,7 @@ case 'REFRESH_GPC_CARD_BALANCE': { if (typeof refreshGpcCardBalance !== 'function') { - throw new Error('GPC API Key 余额查询能力尚未接入。'); + throw new Error('GPC 卡密查询能力尚未接入。'); } const state = await getState(); const result = await refreshGpcCardBalance({ diff --git a/core/flow-kernel/logging-status.js b/core/flow-kernel/logging-status.js index d351908..519f08a 100644 --- a/core/flow-kernel/logging-status.js +++ b/core/flow-kernel/logging-status.js @@ -92,7 +92,7 @@ function getErrorMessage(error) { return String(typeof error === 'string' ? error : error?.message || '') - .replace(/^GPC_TASK_ENDED::/i, '') + .replace(/^GPC_PAGE_FLOW_ENDED::/i, '') .replace(/^AUTO_RUN_STEP_IDLE_RESTART::/i, ''); } diff --git a/core/flow-kernel/runtime-state.js b/core/flow-kernel/runtime-state.js index 4dd4faf..d41d38f 100644 --- a/core/flow-kernel/runtime-state.js +++ b/core/flow-kernel/runtime-state.js @@ -58,33 +58,8 @@ 'plusManualConfirmationMethod', 'plusManualConfirmationTitle', 'plusManualConfirmationMessage', - 'gopayHelperReferenceId', - 'gopayHelperGoPayGuid', - 'gopayHelperRedirectUrl', - 'gopayHelperNextAction', - 'gopayHelperFlowId', - 'gopayHelperChallengeId', - 'gopayHelperStartPayload', - 'gopayHelperTaskId', - 'gopayHelperTaskStatus', - 'gopayHelperStatusText', - 'gopayHelperRemoteStage', - 'gopayHelperApiWaitingFor', - 'gopayHelperApiInputDeadlineAt', - 'gopayHelperApiInputWaitSeconds', - 'gopayHelperLastInputError', - 'gopayHelperOtpInvalidCount', - 'gopayHelperFailureStage', - 'gopayHelperFailureDetail', - 'gopayHelperTaskPayload', - 'gopayHelperOrderCreatedAt', - 'gopayHelperTaskProgressSignature', - 'gopayHelperTaskProgressAt', - 'gopayHelperTaskProgressTaskId', - 'gopayHelperPinPayload', - 'gopayHelperResolvedOtp', - 'gopayHelperOtpRequestId', - 'gopayHelperOtpReferenceId', + 'gpcPageStatus', + 'gpcPageStatusText', ]), phoneVerification: Object.freeze([ 'currentPhoneActivation', diff --git a/docs/使用教程/使用教程.md b/docs/使用教程/使用教程.md index ec4741a..4b2a4f0 100644 --- a/docs/使用教程/使用教程.md +++ b/docs/使用教程/使用教程.md @@ -28,7 +28,7 @@ | 06 | PayPal 注册与绑卡使用教程 | `PayPal`、注册、绑卡、钱包、身份认证、右上角通知 | `docs/使用教程/分部分/06-PayPal-注册与绑卡使用教程.md` | | 07 | ChatGPT Plus 订阅说明(待人工审核) | `ChatGPT Plus`、订阅说明、支付流程说明、人工审核 | `docs/使用教程/分部分/07-0元试用-ChatGPT-Plus-教程.md` | | 08 | Clash Verge 非港轮询配置 | `Clash Verge`、`非港轮询`、扩展脚本、规则模式、系统代理 | `docs/使用教程/分部分/08-Clash-Verge-非港轮询配置.md` | -| 09 | GPC 卡密与 API 使用说明 | `GPC`、充值卡密、`API` 卡密、`API Key`、自动化扩展、短信 `Helper` | `docs/使用教程/分部分/09-GPC-卡密与-API-使用说明.md` | +| 09 | GPC 卡密页面充值说明 | `GPC`、充值卡密、卡密余额、网页充值、`ChatGPT Plus` | `docs/使用教程/分部分/09-GPC-卡密页面充值说明.md` | --- @@ -69,7 +69,7 @@ 3. 根据你当前使用的邮箱方案,选择 `03`、`04`、`05` 4. 如果需要支付和订阅,再看 `06`、`07` 5. 如果需要代理配置,再看 `08` -6. 如果需要 `GPC` 卡密、`API Key` 或自动化接入,再看 `09` +6. 如果需要使用 `GPC` 卡密网页充值,再看 `09` --- diff --git a/docs/使用教程/使用教程书写模板.md b/docs/使用教程/使用教程书写模板.md index 1908cca..730bb8d 100644 --- a/docs/使用教程/使用教程书写模板.md +++ b/docs/使用教程/使用教程书写模板.md @@ -78,7 +78,7 @@ AI 维护时默认按下面的范围归类: | 06 | PayPal 注册与绑卡使用教程 | `PayPal`、注册、绑卡、钱包、身份认证、右上角通知 | `docs/使用教程/分部分/06-PayPal-注册与绑卡使用教程.md` | | 07 | ChatGPT Plus 订阅说明(待人工审核) | `ChatGPT Plus`、订阅说明、支付流程说明、人工审核 | `docs/使用教程/分部分/07-0元试用-ChatGPT-Plus-教程.md` | | 08 | Clash Verge 非港轮询配置 | `Clash Verge`、`非港轮询`、扩展脚本、规则模式、系统代理 | `docs/使用教程/分部分/08-Clash-Verge-非港轮询配置.md` | -| 09 | GPC 卡密与 API 使用说明 | `GPC`、充值卡密、`API` 卡密、`API Key`、自动化扩展、短信 `Helper` | `docs/使用教程/分部分/09-GPC-卡密与-API-使用说明.md` | +| 09 | GPC 卡密页面充值说明 | `GPC`、充值卡密、卡密余额、网页充值、`ChatGPT Plus` | `docs/使用教程/分部分/09-GPC-卡密页面充值说明.md` | --- diff --git a/docs/使用教程/分部分/09-GPC-卡密与-API-使用说明.md b/docs/使用教程/分部分/09-GPC-卡密与-API-使用说明.md deleted file mode 100644 index ee4b69e..0000000 --- a/docs/使用教程/分部分/09-GPC-卡密与-API-使用说明.md +++ /dev/null @@ -1,129 +0,0 @@ -# 第九部分:GPC 卡密与 API 使用说明 - -## 部分信息 - -- `section_slug`: `gpc-card-code-and-api` -- `适用主题`: `GPC`、充值卡密、`API` 卡密、`API Key`、自动化扩展、短信 `Helper` -- `维护方式`: `直接更新本文件` - -## 适用场景 - -- 需要区分 `GPC` 充值卡密和 `API` 卡密 -- 号商需要给指定账号直接开通 `ChatGPT Plus` -- 自动化扩展插件需要接入 `GPC` 模式 -- 中转站、账号系统或反代系统需要批量自动注册、补号和订阅 -- 需要配合短信 `Helper` 自动补号、自动提交验证码 - -## 准备内容 - -- 已获取对应类型的 `GPC` 卡密 -- 可以打开 `GPC` 前台 -- 如果使用 `API` 模式,需要准备自动化扩展插件 -- 如果需要全自动补号,需要准备并配置短信 `Helper` -- 如果还没有 `API` 次数套餐,可以先打开购买页面:[https://pay.ldxp.cn/item/9u90k7](https://pay.ldxp.cn/item/9u90k7) - -## 核心区别 - -| 类型 | 适合对象 | 主要用途 | 使用方式 | -|---|---|---|---| -| 充值卡密 | 号商、人工直冲用户 | 给指定账号直接开通 `ChatGPT Plus` | 在 `GPC` 前台选择充值卡密登录后创建充值任务 | -| `API` 卡密 | 自动化插件、中转站、反代系统 | 批量自动补号、自动注册、自动订阅 | 先兑换成 `API Key`,再填入自动化扩展或系统 | - -简单理解: - -- 充值卡密 = 给某个账号直接充值 `Plus` -- `API` 卡密 = 兑换接口额度,用于自动化扩展或系统集成 - -## 核心优势 - -- 私有协议链路,流程更稳定 -- 授权 `CODEX` 后,99% 场景不风控、不弹 `add-phone` -- 无需额外接码平台,配合短信 `Helper` 即可自动处理验证码 -- `API` 模式适合自动化补号、批量注册、批量订阅和反代系统接入 -- 充值卡密适合单账号直冲,`API` 卡密适合系统化批量接入 - -## 操作步骤 - -### 方案一:使用充值卡密直冲 - -充值卡密主要用于给指定账号直接开通 `ChatGPT Plus`,适合人工或半自动的账号直冲场景。客户只需要提供要充值账号的授权信息,不需要自己准备 `GoPay`。 - -#### 第一步:获取充值卡密 - -先确认手里拿到的是充值卡密,而不是 `API` 卡密。 - -#### 第二步:打开 `GPC` 前台 - -进入 `GPC` 前台页面后,选择充值卡密相关入口。 - -#### 第三步:选择充值卡密登录 - -在登录方式中选择 `充值卡密登录`,然后输入充值卡密。 - -#### 第四步:填写要充值账号的授权信息 - -按照页面提示输入需要开通 `Plus` 的账号授权信息。 - -#### 第五步:创建充值任务 - -确认账号信息无误后创建充值任务,并等待系统执行。 - -#### 第六步:确认任务结果 - -任务成功后,会扣减 1 次卡密次数。此时对应账号的 `ChatGPT Plus` 开通流程即完成。 - -### 方案二:使用 `API` 卡密接入自动化扩展 - -`API` 卡密本身不是直接拿来请求任务接口的。使用前需要先兑换出 `API Key`,后续由自动化插件或系统使用 `API Key` 调用。 - -#### 第一步:购买 `API` 次数套餐 - -打开下面页面购买对应次数套餐: - -[https://pay.ldxp.cn/item/9u90k7](https://pay.ldxp.cn/item/9u90k7) - -#### 第二步:兑换 `API Key` - -购买后打开下面页面兑换: - -[https://gpc.qlhazycoder.top/兑换](https://gpc.qlhazycoder.top/%E5%85%91%E6%8D%A2) - -输入 `API` 卡密后,会获得一个 `API Key`。 - -#### 第三步:填写到自动化扩展 - -打开自动化扩展插件后,按下面顺序配置: - -1. 启用 `GPC 模式` -2. 填写兑换得到的 `API Key` -3. 填写 `GPC` 相关参数 -4. 如需全自动补号,开启并配置短信 `Helper` -5. 保存配置 - -配置保存后,自动化扩展即可开始执行注册账号和开通流程。 - -## 常见问题 - -### 充值卡密可以填到自动化扩展里吗? - -不建议。充值卡密主要用于 `GPC` 前台的账号直冲流程;自动化扩展应使用 `API` 卡密兑换出来的 `API Key`。 - -### `API` 卡密可以直接请求任务接口吗? - -不可以。`API` 卡密需要先到兑换页面换成 `API Key`,后续请求接口或填写扩展配置时使用的是 `API Key`。 - -### 忘记 `API Key` 怎么办? - -可以回到兑换页面,用原来的 `API` 卡密再次查看对应的 `API Key`。 - -### 什么时候会扣减次数? - -充值卡密通常在充值任务成功后扣减 1 次。`API` 模式的次数扣减以接口任务执行结果和系统记录为准。 - -## 注意事项 - -- 充值卡密和 `API` 卡密用途不同,请不要混用 -- `API` 卡密需要先兑换成 `API Key`,再填入自动化扩展 -- `API Key` 请妥善保存,不要公开泄露 -- 忘记 `API Key` 时,可以回到兑换页面用原 `API` 卡密再次查看 -- 全自动补号场景需要同时确认短信 `Helper` 已正确启用 diff --git a/docs/使用教程/分部分/09-GPC-卡密页面充值说明.md b/docs/使用教程/分部分/09-GPC-卡密页面充值说明.md new file mode 100644 index 0000000..ae6eafc --- /dev/null +++ b/docs/使用教程/分部分/09-GPC-卡密页面充值说明.md @@ -0,0 +1,66 @@ +# 第九部分:GPC 卡密页面充值说明 + +## 部分信息 + +- `section_slug`: `gpc-card-page-recharge` +- `适用主题`: `GPC`、充值卡密、卡密余额、网页充值、`ChatGPT Plus` +- `维护方式`: `直接更新本文件` + +## 适用场景 + +- 需要在扩展中使用 `GPC` 卡密开通 `ChatGPT Plus` +- 需要自动打开 `GPC` 页面、填写卡密和 ChatGPT session +- 需要在充值前查看卡密格式、状态和剩余次数 + +## 准备内容 + +- 一个格式类似 `GPC-6C9F1A32-45734795-914E6F00` 的 `GPC` 卡密 +- 当前浏览器中可访问 [https://gpc.qlhazycoder.top/](https://gpc.qlhazycoder.top/) +- 当前 ChatGPT 账号已经登录,并能正常访问 `https://chatgpt.com/api/auth/session` +- 如果还没有卡密,可以通过侧边栏的 `购买卡密` 按钮打开购买入口 + +## 扩展中的配置 + +1. 打开侧边栏并启用 `Plus 模式` +2. 在 `Plus 支付` 中选择 `GPC` +3. 在 `GPC 卡密` 输入框填写卡密 +4. 停止输入 1 秒后,扩展会自动检查格式并查询剩余次数 +5. 状态显示正常后,再启动自动流程或手动执行 GPC 步骤 + +## 自动流程 + +### 第六步:打开 GPC 页面并准备 + +扩展会打开或复用 `GPC` 页面,确认页面中的卡密输入框内容和侧边栏保存的卡密一致;如果不一致,会直接覆盖为当前卡密。 + +同一步还会异步读取 ChatGPT session 的完整 JSON 内容,并填入 `GPC` 页面左下角的文本框;如果页面里已有旧内容,也会覆盖为本轮读取到的 session。 + +准备完成后,流程进入下一步。 + +### 第七步:启动并等待 GPC 完成 + +扩展会点击 `开始plus充值`,页面按钮会进入 `任务进行中` 状态。随后扩展持续读取右侧日志和按钮文案: + +- 如果日志出现 `订阅完成`,并且按钮恢复为 `开始plus充值`,本轮 GPC 充值完成 +- 如果没有出现 `订阅完成`,但按钮恢复为 `开始plus充值`,扩展会再次点击启动 +- 如果日志出现 `该账户没有试用资格`,当前轮直接失败 +- 如果长时间没有完成,会按后台超时规则结束当前 GPC 页面流程 + +## 常见问题 + +### 卡密格式不正确怎么办? + +扩展只接受 `GPC-XXXXXXXX-XXXXXXXX-XXXXXXXX` 这种格式,其中 `X` 为大写或小写的十六进制字符。输入后扩展会自动转成大写。 + +### 为什么没有自动查询余额? + +余额检测会在停止输入约 1 秒后触发。没有填写卡密、格式不正确、页面接口不可用或网络失败时,状态区会显示对应错误。 + +### GPC 页面已有旧卡密或旧 session 会怎样? + +第六步会以侧边栏当前保存的卡密和本轮读取到的 ChatGPT session 为准,页面已有内容会被覆盖。 + +## 注意事项 + +- 当前扩展只保留 `GPC` 网页充值链路和卡密余额检测 +- 充值是否成功以 `GPC` 页面日志中的 `订阅完成` 为准 diff --git a/flows/openai/background/steps/create-plus-checkout.js b/flows/openai/background/steps/create-plus-checkout.js index 135693d..a0e6417 100644 --- a/flows/openai/background/steps/create-plus-checkout.js +++ b/flows/openai/background/steps/create-plus-checkout.js @@ -15,9 +15,8 @@ const LOCAL_CHECKOUT_PROXY_SETTINGS_SCOPE = 'regular'; const LOCAL_CHECKOUT_PROXY_TIMEOUT_MS = 1200; const LOCAL_CHECKOUT_PROXY_SETTLE_MS = 350; - const DEFAULT_GPC_HELPER_API_URL = 'https://gpc.qlhazycoder.top'; - const GPC_HELPER_PHONE_MODE_AUTO = 'auto'; - const GPC_HELPER_PHONE_MODE_MANUAL = 'manual'; + const DEFAULT_GPC_BASE_URL = 'https://gpc.qlhazycoder.top'; + const GPC_PORTAL_URL = 'https://gpc.qlhazycoder.top/'; const HOSTED_CHECKOUT_ADDRESS_ENDPOINT = 'https://www.meiguodizhi.com/api/v1/dz'; const HOSTED_CHECKOUT_SUCCESS_URL_PATTERN = /^https:\/\/(?:chatgpt\.com|www\.chatgpt\.com|chat\.openai\.com)\/(?:backend-api\/)?payments\/success(?:[/?#]|$)/i; const HOSTED_CHECKOUT_TRANSITION_TIMEOUT_MS = 120000; @@ -1221,208 +1220,16 @@ function FindProxyForURL(url, host) { } } - function normalizeHelperCountryCode(countryCode = '86') { - const digits = String(countryCode || '').replace(/\D/g, ''); - return digits || '86'; - } - - 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 normalizeGpcHelperPhoneMode(value = '') { + function normalizeGpcBaseUrl(apiUrl = '') { const rootScope = typeof self !== 'undefined' ? self : globalThis; - if (rootScope.GoPayUtils?.normalizeGpcHelperPhoneMode) { - return rootScope.GoPayUtils.normalizeGpcHelperPhoneMode(value); + if (rootScope.GoPayUtils?.normalizeGpcBaseUrl) { + return rootScope.GoPayUtils.normalizeGpcBaseUrl(apiUrl); } - const normalized = String(value || '').trim().toLowerCase(); - return normalized === GPC_HELPER_PHONE_MODE_AUTO || normalized === 'builtin' - ? GPC_HELPER_PHONE_MODE_AUTO - : GPC_HELPER_PHONE_MODE_MANUAL; - } - - function normalizeGpcOtpChannel(value = '') { - const rootScope = typeof self !== 'undefined' ? self : globalThis; - if (rootScope.GoPayUtils?.normalizeGpcOtpChannel) { - return rootScope.GoPayUtils.normalizeGpcOtpChannel(value); - } - return String(value || '').trim().toLowerCase() === 'sms' ? 'sms' : 'whatsapp'; - } - - function resolveGpcHelperApiKey(state = {}) { - const apiKey = String( - state?.gopayHelperApiKey - || state?.gpcApiKey - || state?.apiKey - || '' - ).trim(); - if (!apiKey) { - throw new Error('创建 GPC 订单失败:缺少 API Key。'); - } - return apiKey; - } - - function normalizeGpcHelperBaseUrl(apiUrl = '') { - const rootScope = typeof self !== 'undefined' ? self : globalThis; - if (rootScope.GoPayUtils?.normalizeGpcHelperBaseUrl) { - return rootScope.GoPayUtils.normalizeGpcHelperBaseUrl(apiUrl); - } - let normalized = String(apiUrl || DEFAULT_GPC_HELPER_API_URL).trim().replace(/\/+$/g, ''); + let normalized = String(apiUrl || DEFAULT_GPC_BASE_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\/web\/card\/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 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}`; - } - - 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 buildGpcBalanceUrl(apiUrl = '') { - const rootScope = typeof self !== 'undefined' ? self : globalThis; - if (rootScope.GoPayUtils?.buildGpcApiKeyBalanceUrl) { - return rootScope.GoPayUtils.buildGpcApiKeyBalanceUrl(apiUrl); - } - if (rootScope.GoPayUtils?.buildGpcCardBalanceUrl) { - return rootScope.GoPayUtils.buildGpcCardBalanceUrl(apiUrl); - } - return buildGpcHelperApiUrl(apiUrl, '/api/gp/balance'); - } - - 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}`; - } - - function getGpcRemainingUses(payload = {}) { - const rootScope = typeof self !== 'undefined' ? self : globalThis; - if (rootScope.GoPayUtils?.getGpcBalanceRemainingUses) { - return rootScope.GoPayUtils.getGpcBalanceRemainingUses(payload); - } - const data = unwrapGpcResponse(payload); - const numeric = Number(data?.remaining_uses ?? data?.remainingUses ?? data?.balance ?? data?.remaining); - return Number.isFinite(numeric) ? Math.max(0, Math.floor(numeric)) : null; - } - - function normalizeGpcAutoModePermissionValue(value) { - if (typeof value === 'boolean') { - return value; - } - if (typeof value === 'number') { - if (value === 1) return true; - if (value === 0) return false; - } - const normalized = String(value ?? '').trim().toLowerCase(); - if (!normalized) { - return null; - } - if (['true', '1', 'yes', 'y', 'on', 'enabled', 'enable'].includes(normalized)) { - return true; - } - if (['false', '0', 'no', 'n', 'off', 'disabled', 'disable'].includes(normalized)) { - return false; - } - return null; - } - - function getGpcAutoModePermission(payload = {}) { - const data = unwrapGpcResponse(payload); - if (!data || typeof data !== 'object' || Array.isArray(data)) { - return null; - } - return normalizeGpcAutoModePermissionValue( - data.auto_mode_enabled - ?? data.autoModeEnabled - ?? data.auto_enabled - ?? data.autoEnabled - ); - } - - function isGpcAutoModePermissionDenied(payload = {}) { - return getGpcAutoModePermission(payload) === false; - } - - async function assertGpcApiKeyReadyForCreate(state = {}, phoneMode = GPC_HELPER_PHONE_MODE_MANUAL, apiKey = '') { - const apiUrl = buildGpcBalanceUrl(state?.gopayHelperApiUrl); - if (!apiUrl) { - throw new Error('创建 GPC 订单失败:缺少 API 地址。'); - } - const { response, data } = await fetchJsonWithTimeout(apiUrl, { - method: 'GET', - headers: { - Accept: 'application/json', - 'X-API-Key': apiKey, - }, - }, 30000); - if (!response?.ok || !isGpcUnifiedResponseOk(data)) { - const detail = getGpcResponseErrorDetail(data, response?.status || 0); - throw new Error(`创建 GPC 订单失败:API Key 校验失败:${detail}`); - } - const balanceData = unwrapGpcResponse(data); - const remainingUses = getGpcRemainingUses(balanceData); - const status = String(balanceData?.status || balanceData?.card_status || balanceData?.cardStatus || '').trim().toLowerCase(); - if (status && status !== 'active') { - throw new Error(`创建 GPC 订单失败:API Key 状态不可用(${status})。`); - } - if (remainingUses !== null && remainingUses <= 0) { - throw new Error('创建 GPC 订单失败:API Key 剩余次数不足。'); - } - if (phoneMode === GPC_HELPER_PHONE_MODE_AUTO && isGpcAutoModePermissionDenied(balanceData)) { - throw new Error('创建 GPC 订单失败:当前 GPC API Key 未开通自动模式。'); - } + return normalized || DEFAULT_GPC_BASE_URL; } async function fetchJsonWithTimeout(url, options = {}, timeoutMs = 30000) { @@ -1430,13 +1237,13 @@ function FindProxyForURL(url, host) { ? fetchImpl : (typeof fetch === 'function' ? fetch.bind(globalThis) : null); if (typeof fetcher !== 'function') { - throw new Error('当前运行环境不支持 fetch,无法调用 GPC API。'); + throw new Error('当前运行环境不支持 fetch,无法调用远端接口。'); } const controller = typeof AbortController === 'function' ? new AbortController() : null; const effectiveTimeoutMs = Math.max(1000, Number(timeoutMs) || 30000); let didTimeout = false; let timer = null; - const buildTimeoutError = () => new Error(`GPC API 请求超时(>${Math.round(effectiveTimeoutMs / 1000)} 秒):${url}`); + const buildTimeoutError = () => new Error(`远端接口请求超时(>${Math.round(effectiveTimeoutMs / 1000)} 秒):${url}`); const timeoutPromise = new Promise((_, reject) => { timer = setTimeout(() => { didTimeout = true; @@ -1466,13 +1273,100 @@ function FindProxyForURL(url, host) { } } - async function readAccessTokenFromChatGptSessionTab(tabId) { + function resolveGpcCardKey(state = {}) { + const cardKey = String(state?.gpcCardKey || '').trim(); + if (!cardKey) { + throw new Error('步骤 6:GPC 模式缺少卡密,请先在侧边栏填写 GPC 卡密。'); + } + return cardKey; + } + + function buildGpcPortalUrl(state = {}) { + const baseUrl = normalizeGpcBaseUrl(state?.gpcBaseUrl || DEFAULT_GPC_BASE_URL) + .replace(/\/+$/g, ''); + return `${baseUrl || DEFAULT_GPC_BASE_URL}/`; + } + + function isGpcPortalUrl(url = '') { + const text = String(url || '').trim(); + if (!text) { + return false; + } + try { + const parsed = new URL(text); + return parsed.hostname === 'gpc.qlhazycoder.top'; + } catch { + return /^https:\/\/gpc\.qlhazycoder\.top(?:\/|$)/i.test(text); + } + } + + async function findOpenGpcPortalTabId(portalUrl = GPC_PORTAL_URL) { + const queryTabs = typeof queryTabsInAutomationWindow === 'function' + ? queryTabsInAutomationWindow + : (chrome?.tabs?.query ? (queryInfo) => chrome.tabs.query(queryInfo) : null); + if (typeof queryTabs !== 'function') { + return 0; + } + const tabs = await queryTabs({}).catch(() => []); + const candidates = (Array.isArray(tabs) ? tabs : []) + .filter((tab) => Number.isInteger(tab?.id) && isGpcPortalUrl(tab.url || '')); + if (!candidates.length) { + return 0; + } + const match = candidates.find((tab) => tab.active && tab.currentWindow) + || candidates.find((tab) => tab.active) + || candidates[0]; + if (match?.id && chrome?.tabs?.update) { + await chrome.tabs.update(match.id, { url: portalUrl, active: true }).catch(() => ( + chrome.tabs.update(match.id, { active: true }).catch(() => {}) + )); + } + return match?.id || 0; + } + + async function openOrReuseGpcPortalTab(state = {}) { + const portalUrl = buildGpcPortalUrl(state); + const storedTab = await getTabById(Number(state?.plusCheckoutTabId) || 0); + if (storedTab?.id && isGpcPortalUrl(storedTab.url || '')) { + if (chrome?.tabs?.update) { + await chrome.tabs.update(storedTab.id, { url: portalUrl, active: true }).catch(() => ( + chrome.tabs.update(storedTab.id, { active: true }).catch(() => {}) + )); + } + if (typeof registerTab === 'function') { + await registerTab(PLUS_CHECKOUT_SOURCE, storedTab.id); + } + return { tabId: storedTab.id, portalUrl }; + } + + const existingTabId = await findOpenGpcPortalTabId(portalUrl); + if (existingTabId) { + if (typeof registerTab === 'function') { + await registerTab(PLUS_CHECKOUT_SOURCE, existingTabId); + } + return { tabId: existingTabId, portalUrl }; + } + + const tab = typeof createAutomationTab === 'function' + ? await createAutomationTab({ url: portalUrl, active: true }) + : await chrome.tabs.create({ url: portalUrl, active: true }); + const tabId = Number(tab?.id); + if (!Number.isInteger(tabId)) { + throw new Error('步骤 6:打开 GPC 页面失败。'); + } + if (typeof registerTab === 'function') { + await registerTab(PLUS_CHECKOUT_SOURCE, tabId); + } + return { tabId, portalUrl }; + } + + async function readSessionFromChatGptSessionTab(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...', + logMessage: '步骤 6:正在等待 ChatGPT 页面完成加载,再继续获取 session...', }); const sessionResult = await sendTabMessageUntilStopped(tabId, PLUS_CHECKOUT_SOURCE, { @@ -1486,125 +1380,148 @@ function FindProxyForURL(url, host) { if (sessionResult?.error) { throw new Error(sessionResult.error); } - return String(sessionResult?.accessToken || sessionResult?.session?.accessToken || '').trim(); + const session = sessionResult?.session && typeof sessionResult.session === 'object' + ? sessionResult.session + : null; + const accessToken = String(sessionResult?.accessToken || session?.accessToken || '').trim(); + if (!session || !accessToken) { + throw new Error('步骤 6:GPC 模式获取 ChatGPT session 失败。'); + } + return session; } - async function generateGpcCheckoutFromApi(accessToken = '', state = {}) { - const token = String(accessToken || '').trim(); - if (!token) { - throw new Error('创建 GPC 订单失败:缺少 accessToken。'); - } - const apiUrl = buildGpcTaskCreateUrl(state?.gopayHelperApiUrl); - if (!apiUrl) { - throw new Error('创建 GPC 订单失败:缺少 API 地址。'); - } - const phoneMode = normalizeGpcHelperPhoneMode(state?.gopayHelperPhoneMode || state?.phoneMode); - const isAutoMode = phoneMode === GPC_HELPER_PHONE_MODE_AUTO; - const phoneNumber = String(state?.gopayHelperPhoneNumber || '').trim(); - const countryCode = normalizeHelperCountryCode(state?.gopayHelperCountryCode || '86'); - const pin = String(state?.gopayHelperPin || '').trim(); - const apiKey = resolveGpcHelperApiKey(state); - if (!isAutoMode && !phoneNumber) { - throw new Error('创建 GPC 订单失败:手动模式缺少手机号。'); - } - if (!isAutoMode && !pin) { - throw new Error('创建 GPC 订单失败:手动模式缺少 PIN。'); + async function prepareGpcPortalPage(tabId, cardKey = '', sessionJson = '') { + if (!chrome?.scripting?.executeScript) { + throw new Error('步骤 6:当前运行环境不支持脚本注入,无法填写 GPC 页面。'); } + await waitForTabCompleteUntilStopped(tabId); + await sleepWithStop(800); + const results = await chrome.scripting.executeScript({ + target: { tabId }, + func: (rawCardKey, rawSessionJson) => { + const textOf = (element) => String(element?.innerText || element?.textContent || element?.value || '').replace(/\s+/g, ' ').trim(); + const isVisible = (element) => { + if (!element) return false; + const style = window.getComputedStyle ? window.getComputedStyle(element) : null; + if (style && (style.display === 'none' || style.visibility === 'hidden' || Number(style.opacity) === 0)) { + return false; + } + const rect = typeof element.getBoundingClientRect === 'function' ? element.getBoundingClientRect() : null; + return !rect || rect.width > 0 || rect.height > 0; + }; + const dispatchInput = (element, value) => { + element.focus?.(); + element.value = value; + element.dispatchEvent?.(new Event('input', { bubbles: true })); + element.dispatchEvent?.(new Event('change', { bubbles: true })); + element.blur?.(); + }; + const clickElement = (element) => { + element.scrollIntoView?.({ block: 'center', inline: 'center' }); + element.click?.(); + }; + const modeButtons = Array.from(document.querySelectorAll('button, [role="button"], .design-mode-card, .mode-card')); + const cardModeButton = modeButtons.find((element) => /卡密充值/.test(textOf(element))); + if (cardModeButton && !/\bactive\b/i.test(String(cardModeButton.className || ''))) { + clickElement(cardModeButton); + } else if (cardModeButton) { + clickElement(cardModeButton); + } - throwIfStopped(); - await assertGpcApiKeyReadyForCreate(state, phoneMode, apiKey); - throwIfStopped(); - const payload = { - access_token: token, - phone_mode: phoneMode, - }; - if (!isAutoMode) { - payload.country_code = countryCode; - payload.phone_number = normalizeHelperPhoneNumber(phoneNumber, countryCode); - payload.otp_channel = normalizeGpcOtpChannel(state?.gopayHelperOtpChannel); - } + const compactCardKey = String(rawCardKey || '').trim().replace(/\s+/g, ''); + const explicitSegments = compactCardKey.includes('-') || compactCardKey.includes('_') + ? compactCardKey.split(/[-_]+/).filter(Boolean) + : []; + const cardSegments = explicitSegments.length + ? explicitSegments + : (compactCardKey.match(/.{1,8}/g) || []); + const cardInputs = Array.from(document.querySelectorAll('input.card-key-seg, input[placeholder*="XXXXXXXX"], input[maxlength="8"]')) + .filter(isVisible); + cardInputs.forEach((input, index) => { + dispatchInput(input, cardSegments[index] || ''); + }); + if (!cardInputs.length) { + const fallbackInput = Array.from(document.querySelectorAll('input')).find((input) => /卡密|card/i.test([ + input.placeholder, + input.name, + input.id, + input.className, + ].join(' '))); + if (fallbackInput) { + dispatchInput(fallbackInput, compactCardKey); + } + } - const orderCreatedAt = Date.now(); - 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', - 'X-API-Key': apiKey, + const sessionTextarea = Array.from(document.querySelectorAll('textarea')).find((textarea) => /session|accessToken|完整/i.test([ + textarea.placeholder, + textarea.name, + textarea.id, + textarea.className, + ].join(' '))) || document.querySelector('textarea.design-session-input') || document.querySelector('textarea'); + if (!sessionTextarea) { + throw new Error('未找到 GPC session 输入框。'); + } + dispatchInput(sessionTextarea, String(rawSessionJson || '')); + + const startButton = Array.from(document.querySelectorAll('button, [role="button"]')) + .find((button) => /开始\s*Plus\s*充值|任务进行中/.test(textOf(button))); + return { + ok: true, + cardInputCount: cardInputs.length, + cardSegments: cardSegments.map((segment) => segment ? segment.length : 0), + sessionLength: String(sessionTextarea.value || '').length, + startButtonText: textOf(startButton), + activeModeText: textOf(cardModeButton), + url: location.href, + }; }, - body: JSON.stringify(payload), - }, 30000); - - const taskData = unwrapGpcResponse(data); - const taskId = String(taskData?.task_id || taskData?.taskId || '').trim(); - - if (!response?.ok || !isGpcUnifiedResponseOk(data) || !taskId) { - const detail = getGpcResponseErrorDetail(data, response?.status || 0); - throw new Error(`创建 GPC 订单失败:${detail}`); + args: [cardKey, sessionJson], + }); + const result = results?.[0]?.result || {}; + if (!result?.ok) { + throw new Error('步骤 6:GPC 页面准备失败。'); } - - return { - taskId, - taskStatus: String(taskData?.status || '').trim(), - statusText: String(taskData?.status_text || taskData?.statusText || '').trim(), - remoteStage: String(taskData?.remote_stage || taskData?.remoteStage || '').trim(), - orderCreatedAt, - responsePayload: taskData && typeof taskData === 'object' && !Array.isArray(taskData) ? taskData : null, - phoneMode: normalizeGpcHelperPhoneMode(taskData?.phone_mode || taskData?.phoneMode || phoneMode), - country: 'ID', - currency: 'IDR', - checkoutSource: PLUS_PAYMENT_METHOD_GPC_HELPER, - }; + return result; } 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(() => {}); - } + const cardKey = resolveGpcCardKey(state); + await addLog('步骤 6:正在打开 GPC 页面并准备卡密充值模式...', 'info'); + const { tabId, portalUrl } = await openOrReuseGpcPortalTab(state); + await addLog('步骤 6:正在从 ChatGPT 获取完整 session...', 'info'); + const sessionTabId = await openFreshChatGptTabForCheckoutCreate(); + let session = null; + try { + session = await readSessionFromChatGptSessionTab(sessionTabId); + } finally { + if (chrome?.tabs?.remove && Number.isInteger(sessionTabId)) { + await chrome.tabs.remove(sessionTabId).catch(() => {}); } } - if (!accessToken) { - throw new Error('步骤 6:GPC 模式获取 accessToken 失败。'); - } - await addLog('步骤 6:正在调用 GPC 接口创建订单...', 'info'); - const result = await generateGpcCheckoutFromApi(accessToken, state); + const sessionJson = JSON.stringify(session); + if (chrome?.tabs?.update) { + await chrome.tabs.update(tabId, { active: true }).catch(() => {}); + } + await addLog('步骤 6:正在填写 GPC 卡密和 ChatGPT session...', 'info'); + const prepared = await prepareGpcPortalPage(tabId, cardKey, sessionJson); await setState({ - plusCheckoutTabId: null, - plusCheckoutUrl: '', - plusCheckoutCountry: result.country || 'ID', - plusCheckoutCurrency: result.currency || 'IDR', - plusCheckoutSource: result.checkoutSource, - gopayHelperTaskId: result.taskId, - gopayHelperTaskStatus: result.taskStatus, - gopayHelperStatusText: result.statusText, - gopayHelperRemoteStage: result.remoteStage, - gopayHelperTaskPayload: result.responsePayload, - gopayHelperTaskProgressSignature: '', - gopayHelperTaskProgressAt: 0, - gopayHelperTaskProgressTaskId: result.taskId, - gopayHelperReferenceId: '', - gopayHelperGoPayGuid: '', - gopayHelperRedirectUrl: '', - gopayHelperNextAction: '', - gopayHelperFlowId: '', - gopayHelperChallengeId: '', - gopayHelperStartPayload: null, - gopayHelperOrderCreatedAt: result.orderCreatedAt || Date.now(), + plusCheckoutTabId: tabId, + plusCheckoutUrl: portalUrl, + plusCheckoutCountry: 'US', + plusCheckoutCurrency: 'USD', + plusCheckoutSource: PLUS_PAYMENT_METHOD_GPC_HELPER, + gpcPageStatus: 'prepared', + gpcPageStatusText: '页面已准备', }); - await addLog(`步骤 6:GPC ${result.phoneMode === GPC_HELPER_PHONE_MODE_AUTO ? '自动' : '手动'}模式任务已创建(task_id: ${result.taskId}),准备继续下一步。`, 'info'); + await addLog( + `步骤 6:GPC 页面已准备完成(卡密 ${prepared.cardInputCount || 0} 段,session ${prepared.sessionLength || 0} 字符),准备继续下一步。`, + 'ok' + ); await completeNodeFromBackground('plus-checkout-create', { - plusCheckoutCountry: result.country || 'ID', - plusCheckoutCurrency: result.currency || 'IDR', - plusCheckoutSource: result.checkoutSource, + plusCheckoutCountry: 'US', + plusCheckoutCurrency: 'USD', + plusCheckoutSource: PLUS_PAYMENT_METHOD_GPC_HELPER, }); } diff --git a/flows/openai/background/steps/fill-plus-checkout.js b/flows/openai/background/steps/fill-plus-checkout.js index cc5737f..dae8f29 100644 --- a/flows/openai/background/steps/fill-plus-checkout.js +++ b/flows/openai/background/steps/fill-plus-checkout.js @@ -10,29 +10,10 @@ 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://gpc.qlhazycoder.top'; - const GPC_HELPER_PHONE_MODE_AUTO = 'auto'; - const GPC_HELPER_PHONE_MODE_MANUAL = 'manual'; - const GPC_TASK_POLL_INTERVAL_MS = 3000; - const GPC_TASK_STALE_STATUS_TIMEOUT_MS = 60000; - const GPC_REMOTE_STAGE_LABELS = { - auto_otp_wait: '等待自动 OTP', - checkout_order_start: '创建订单', - checkout_start: '创建订单', - completed: '充值完成', - gopay_validate_pin: '校验 PIN', - otp_ready: '等待 PIN', - otp_submitted_local: 'OTP 已提交', - payment_processing: '支付处理中', - pin_submitted_local: 'PIN 已提交', - sms_otp_wait: '等待短信 OTP', - whatsapp_otp_wait: '等待 WhatsApp OTP', - }; - const GPC_WAITING_FOR_LABELS = { - auto_otp: '自动 OTP', - otp: 'OTP', - pin: 'PIN', - }; + const GPC_PORTAL_URL = 'https://gpc.qlhazycoder.top/'; + const GPC_PAGE_POLL_INTERVAL_MS = 3000; + const GPC_PAGE_DEFAULT_TIMEOUT_SECONDS = 900; + const GPC_PAGE_MAX_START_ATTEMPTS = 10; const PAYMENT_METHOD_CONFIGS = { [PLUS_PAYMENT_METHOD_PAYPAL]: { id: PLUS_PAYMENT_METHOD_PAYPAL, @@ -108,14 +89,26 @@ return PLUS_CHECKOUT_URL_PATTERN.test(String(url || '')); } + function isGpcPortalUrl(url = '') { + const text = String(url || '').trim(); + if (!text) { + return false; + } + try { + const parsed = new URL(text); + return parsed.hostname === 'gpc.qlhazycoder.top'; + } catch { + return /^https:\/\/gpc\.qlhazycoder\.top(?:\/|$)/i.test(text); + } + } + function normalizeText(value = '') { 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?.gopayHelperTaskId || state?.gopayHelperReferenceId)); + || normalizeText(state?.plusCheckoutSource) === PLUS_PAYMENT_METHOD_GPC_HELPER; } function compactCountryText(value = '') { @@ -134,1025 +127,220 @@ return normalized === PLUS_PAYMENT_METHOD_GOPAY ? PLUS_PAYMENT_METHOD_GOPAY : PLUS_PAYMENT_METHOD_PAYPAL; } - function normalizeGpcHelperPhoneMode(value = '') { - const rootScope = typeof self !== 'undefined' ? self : globalThis; - if (rootScope.GoPayUtils?.normalizeGpcHelperPhoneMode) { - return rootScope.GoPayUtils.normalizeGpcHelperPhoneMode(value); - } - const normalized = String(value || '').trim().toLowerCase(); - return normalized === GPC_HELPER_PHONE_MODE_AUTO || normalized === 'builtin' - ? GPC_HELPER_PHONE_MODE_AUTO - : GPC_HELPER_PHONE_MODE_MANUAL; - } - - function formatGpcRemoteStageLabel(stage = '') { - const normalized = String(stage || '').trim().toLowerCase(); - if (!normalized) { - return ''; - } - return GPC_REMOTE_STAGE_LABELS[normalized] || normalized; - } - - function formatGpcWaitingForLabel(waitingFor = '') { - const normalized = String(waitingFor || '').trim().toLowerCase(); - if (!normalized) { - return ''; - } - return GPC_WAITING_FOR_LABELS[normalized] || normalized.toUpperCase(); - } - - function formatGpcTaskStatusLog(task = {}) { - const statusText = String(task?.status_text || task?.statusText || '').trim(); - const status = String(task?.status || '').trim(); - const remoteStage = String(task?.remote_stage || task?.remoteStage || '').trim(); - const stageText = formatGpcRemoteStageLabel(remoteStage); - const waitingForText = formatGpcWaitingForLabel(task?.api_waiting_for || task?.apiWaitingFor || ''); - const mainText = stageText || statusText || status || '处理中'; - const parts = [`步骤 7:GPC 任务状态:${mainText}`]; - if (waitingForText && !mainText.includes(waitingForText)) { - parts.push(`,等待 ${waitingForText}`); - } - return parts.join(''); - } - - function getGpcHelperPhoneMode(state = {}, task = null) { - return normalizeGpcHelperPhoneMode( - task?.phone_mode - || task?.phoneMode - || state?.gopayHelperPhoneMode - || state?.phoneMode - ); - } - 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 || 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 getGpcPageTimeoutMs(state = {}) { + const seconds = Math.max(1, Math.min(1800, Number(state?.gpcPageTimeoutSeconds) || GPC_PAGE_DEFAULT_TIMEOUT_SECONDS)); + return seconds * 1000; } - 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。'); + async function getAliveGpcPortalTabId(tabId) { + const normalizedTabId = Number(tabId) || 0; + if (!normalizedTabId) { + return null; } - const controller = typeof AbortController === 'function' ? new AbortController() : null; - const effectiveTimeoutMs = Math.max(1000, Number(timeoutMs) || 30000); - let didTimeout = false; - let timer = null; - const buildTimeoutError = () => new Error(`GPC API 请求超时(>${Math.round(effectiveTimeoutMs / 1000)} 秒):${url}`); - const timeoutPromise = new Promise((_, reject) => { - timer = setTimeout(() => { - didTimeout = true; - reject(buildTimeoutError()); - if (controller) { - controller.abort(); - } - }, effectiveTimeoutMs); + if (!chrome?.tabs?.get) { + return normalizedTabId; + } + const tab = await chrome.tabs.get(normalizedTabId).catch(() => null); + return tab && isGpcPortalUrl(tab.url || '') ? normalizedTabId : null; + } + + async function getCurrentGpcPortalTabId() { + const queryTabs = typeof queryTabsInAutomationWindow === 'function' + ? queryTabsInAutomationWindow + : (chrome?.tabs?.query ? (queryInfo) => chrome.tabs.query(queryInfo) : null); + if (typeof queryTabs !== 'function') { + return null; + } + const activeTabs = await queryTabs({ active: true, currentWindow: true }).catch(() => []); + const activeGpcTab = (Array.isArray(activeTabs) ? activeTabs : []) + .find((tab) => Number.isInteger(tab?.id) && isGpcPortalUrl(tab.url || '')); + if (activeGpcTab) { + return activeGpcTab.id; + } + const tabs = await queryTabs({}).catch(() => []); + const gpcTab = (Array.isArray(tabs) ? tabs : []) + .find((tab) => Number.isInteger(tab?.id) && isGpcPortalUrl(tab.url || '')); + return gpcTab?.id || null; + } + + async function getGpcPortalTabId(state = {}) { + const registeredTabId = typeof getTabId === 'function' + ? await Promise.resolve(getTabId(PLUS_CHECKOUT_SOURCE)).catch(() => null) + : null; + if (registeredTabId) { + const aliveRegisteredTabId = await getAliveGpcPortalTabId(registeredTabId); + if (aliveRegisteredTabId) { + return aliveRegisteredTabId; + } + } + const storedTabId = await getAliveGpcPortalTabId(Number(state.plusCheckoutTabId) || 0); + if (storedTabId) { + return storedTabId; + } + const currentTabId = await getCurrentGpcPortalTabId(); + if (currentTabId) { + await addLog('步骤 7:检测到当前已在 GPC 页面,直接接管当前标签页。', 'info'); + return currentTabId; + } + throw new Error('步骤 7:未找到 GPC 页面。请先执行步骤 6 打开并准备 GPC 页面。'); + } + + async function inspectGpcPortalPage(tabId) { + if (!chrome?.scripting?.executeScript) { + throw new Error('步骤 7:当前运行环境不支持脚本注入,无法读取 GPC 页面。'); + } + const results = await chrome.scripting.executeScript({ + target: { tabId }, + func: () => { + const textOf = (element) => String(element?.innerText || element?.textContent || element?.value || '').replace(/\s+/g, ' ').trim(); + const buttons = Array.from(document.querySelectorAll('button, [role="button"]')); + const startButton = buttons.find((button) => /开始\s*Plus\s*充值|任务进行中/.test(textOf(button))) + || buttons.find((button) => /开始|任务/.test(textOf(button))); + const bodyText = String(document.body?.innerText || document.documentElement?.innerText || '').replace(/\r/g, ''); + const logNodes = Array.from(document.querySelectorAll('[class*="log"], [id*="log"], .design-log, .logs, pre, code')); + const logText = logNodes.map(textOf).filter(Boolean).join('\n') || bodyText; + const cardInputs = Array.from(document.querySelectorAll('input.card-key-seg, input[placeholder*="XXXXXXXX"], input[maxlength="8"]')); + const sessionTextarea = document.querySelector('textarea.design-session-input') || document.querySelector('textarea'); + return { + url: location.href, + readyState: document.readyState, + bodyText, + logText, + hasSubscriptionDone: /订阅完成/.test(logText) || /订阅完成/.test(bodyText), + noTrial: /该账户没有试用资格|该账号没有试用资格|没有试用资格/.test(logText) || /该账户没有试用资格|该账号没有试用资格|没有试用资格/.test(bodyText), + startButtonText: textOf(startButton), + startButtonDisabled: Boolean(startButton?.disabled || startButton?.getAttribute?.('aria-disabled') === 'true'), + hasStartButton: Boolean(startButton), + cardKeyValue: cardInputs.map((input) => String(input.value || '')).join('-'), + sessionLength: String(sessionTextarea?.value || '').length, + }; + }, }); - try { - const response = await Promise.race([ - fetcher(url, { ...options, ...(controller ? { signal: controller.signal } : {}) }), - timeoutPromise, - ]); - const data = await Promise.race([ - response.json().catch(() => ({})), - timeoutPromise, - ]); - return { response, data }; - } catch (error) { - if (didTimeout || error?.name === 'AbortError') { - throw buildTimeoutError(); - } - throw error; - } finally { - if (timer) clearTimeout(timer); - } + return results?.[0]?.result || {}; } - function buildGpcOtpPayload(input = {}) { - const rootScope = typeof self !== 'undefined' ? self : globalThis; - if (rootScope.GoPayUtils?.buildGpcOtpPayload) { - return rootScope.GoPayUtils.buildGpcOtpPayload(input); + async function clickGpcStartButton(tabId) { + if (!chrome?.scripting?.executeScript) { + throw new Error('步骤 7:当前运行环境不支持脚本注入,无法启动 GPC 页面流程。'); } - const payload = { - reference_id: String(input.reference_id ?? input.referenceId ?? '').trim(), - otp: String(input.otp ?? input.code ?? '').trim().replace(/[^\d]/g, ''), - }; - 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, ''), - }; - 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?.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 = normalizeGpcHelperPhoneMode(task.phone_mode || task.phoneMode || ''); - 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, { - 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({}); - } - - function normalizeLocalSmsHelperBaseUrl(value = '') { - const fallback = 'http://127.0.0.1:18767'; - const rawValue = String(value || fallback).trim(); - try { - const parsed = new URL(rawValue); - if (!['http:', 'https:'].includes(parsed.protocol)) { - return fallback; - } - const endpointPath = parsed.pathname.replace(/\/+$/g, '') || '/'; - if (['/otp', '/latest-otp', '/health'].includes(endpointPath)) { - parsed.pathname = ''; - parsed.search = ''; - parsed.hash = ''; - } - return parsed.toString().replace(/\/$/, ''); - } catch { - return fallback; - } - } - - function normalizeIncomingGpcSmsOtp(payload = {}) { - const candidates = [ - payload?.otp, - payload?.code, - payload?.sms_code, - payload?.smsCode, - payload?.verification_code, - payload?.verificationCode, - ]; - for (const candidate of candidates) { - const normalized = String(candidate || '').trim().replace(/[^\d]/g, ''); - if (/^\d{4,8}$/.test(normalized)) { - return normalized; - } - } - const messageText = String(payload?.message_text || payload?.messageText || payload?.text || '').trim(); - if (messageText) { - const match = messageText.match(/(?:OTP\s*[::]?\s*|#)(\d{4,8})\b|\b(\d{6})\b/i); - if (match) { - return String(match[1] || match[2] || '').trim(); - } - } - return ''; - } - - function normalizeGpcOtpChannel(value = '') { - const rootScope = typeof self !== 'undefined' ? self : globalThis; - if (rootScope.GoPayUtils?.normalizeGpcOtpChannel) { - return rootScope.GoPayUtils.normalizeGpcOtpChannel(value); - } - return String(value || '').trim().toLowerCase() === 'sms' ? 'sms' : 'whatsapp'; - } - - function normalizeEpochMilliseconds(value = 0) { - const rawValue = String(value ?? '').trim(); - if (!rawValue) { - return 0; - } - const numeric = Number(rawValue); - if (Number.isFinite(numeric) && numeric > 0) { - return Math.floor(numeric < 100000000000 ? numeric * 1000 : numeric); - } - const parsed = Date.parse(rawValue); - return Number.isFinite(parsed) ? parsed : 0; - } - - function normalizeLocalSmsHelperCountryCode(value = '') { - const rootScope = typeof self !== 'undefined' ? self : globalThis; - if (rootScope.GoPayUtils?.normalizeGoPayCountryCode) { - return rootScope.GoPayUtils.normalizeGoPayCountryCode(value || '+86'); - } - const digits = String(value || '+86').replace(/\D/g, ''); - return digits ? `+${digits}` : '+86'; - } - - function normalizeLocalSmsHelperPhoneE164(phone = '', countryCode = '+86') { - const rawPhone = String(phone || '').trim(); - if (!rawPhone) { - return ''; - } - const rootScope = typeof self !== 'undefined' ? self : globalThis; - const normalizedCountryCode = normalizeLocalSmsHelperCountryCode(countryCode); - const normalizedPhone = rootScope.GoPayUtils?.normalizeGoPayPhone - ? rootScope.GoPayUtils.normalizeGoPayPhone(rawPhone) - : rawPhone.replace(/[^\d+]/g, ''); - const phoneDigits = normalizedPhone.replace(/\D/g, ''); - if (!phoneDigits) { - return ''; - } - if (normalizedPhone.startsWith('+')) { - return `+${phoneDigits}`; - } - const countryDigits = normalizedCountryCode.replace(/\D/g, '') || '86'; - let nationalNumber = phoneDigits; - if (countryDigits && nationalNumber.startsWith(countryDigits) && nationalNumber.length > countryDigits.length) { - nationalNumber = nationalNumber.slice(countryDigits.length); - } - return `+${countryDigits}${nationalNumber}`; - } - - function buildLocalSmsHelperOtpUrl(state = {}, taskId = '', options = {}) { - const baseUrl = normalizeLocalSmsHelperBaseUrl(state?.gopayHelperLocalSmsHelperUrl); - const url = new URL(`${baseUrl}/latest-otp`); - const normalizedTaskId = String(taskId || '').trim(); - const phoneNumber = normalizeLocalSmsHelperPhoneE164( - state?.gopayHelperPhoneNumber, - state?.gopayHelperCountryCode || '+86' - ); - 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 (normalizedTaskId) { - url.searchParams.set('task_id', normalizedTaskId); - url.searchParams.set('reference_id', normalizedTaskId); - } - if (phoneNumber) { - url.searchParams.set('phone', phoneNumber); - } - url.searchParams.set('consume', '1'); - const effectiveAfterMs = Math.max(orderCreatedAt, afterOverrideMs); - if (effectiveAfterMs > 0) { - url.searchParams.set('after_ms', String(effectiveAfterMs)); - } - return url.toString(); - } - - 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, taskId, options); - let lastMessage = ''; - while (Date.now() <= deadline) { - throwIfStopped(); - try { - const { response, data } = await fetchJsonWithTimeout(requestUrl, { - method: 'GET', - headers: { Accept: 'application/json' }, - }, requestTimeoutMs); - const otp = normalizeIncomingGpcSmsOtp(data || {}); - if (response?.ok && otp) { - await setState({ - gopayHelperResolvedOtp: otp, - gopayHelperSmsOtpPayload: data && typeof data === 'object' && !Array.isArray(data) ? data : null, - }); - if (typeof broadcastDataUpdate === 'function') { - broadcastDataUpdate({ gopayHelperResolvedOtp: otp }); - } - return otp; + const results = await chrome.scripting.executeScript({ + target: { tabId }, + func: () => { + const textOf = (element) => String(element?.innerText || element?.textContent || element?.value || '').replace(/\s+/g, ' ').trim(); + const buttons = Array.from(document.querySelectorAll('button, [role="button"]')); + const button = buttons.find((candidate) => /开始\s*Plus\s*充值/.test(textOf(candidate))); + if (!button) { + return { + clicked: false, + reason: 'missing', + buttonText: textOf(button), + }; } - lastMessage = String(data?.message || data?.status || '').trim(); - } catch (error) { - lastMessage = error?.message || String(error || '未知错误'); - } - if (singleAttempt) { - break; - } - await sleepWithStop(pollIntervalSeconds * 1000); - } - throw new Error(lastMessage || '本地 SMS Helper 等待 OTP 超时。'); - } - - 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, - plusManualConfirmationRequestId: requestId, - plusManualConfirmationStep: 7, - plusManualConfirmationMethod: 'gopay-otp', - plusManualConfirmationTitle: title || 'GPC OTP 验证', - plusManualConfirmationMessage: message || '请输入 OTP 验证码', - 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); - const resolvedOtp = String(currentState?.gopayHelperResolvedOtp || '').trim().replace(/[^\d]/g, ''); - if (resolvedOtp) { - resolve(resolvedOtp); - } else { - reject(new Error('OTP 输入已取消')); - } - } - } catch (error) { - clearInterval(checkInterval); - reject(error); + if (button.disabled || button.getAttribute?.('aria-disabled') === 'true') { + return { + clicked: false, + reason: 'disabled', + buttonText: textOf(button), + }; } - }, 500); - }); - } - - function isGpcTaskManualMode(task = {}, state = {}) { - return getGpcHelperPhoneMode(state, task) === GPC_HELPER_PHONE_MODE_MANUAL; - } - - function isGpcTaskOtpWait(task = {}, state = {}) { - return isGpcTaskManualMode(task, state) && task?.api_waiting_for === 'otp'; - } - - function isGpcTaskPinWait(task = {}, state = {}) { - return isGpcTaskManualMode(task, state) - && (task?.api_waiting_for === 'pin' || task?.status === 'otp_ready'); - } - - function isGpcTaskTerminal(status = '') { - return ['completed', 'failed', 'expired', 'discarded'].includes(String(status || '').trim().toLowerCase()); - } - - function buildGpcTaskProgressSignature(task = {}) { - return [ - task?.status, - task?.status_text, - task?.remote_stage, - task?.api_waiting_for, - task?.last_input_error, - task?.otp_invalid_count, - task?.failure_stage || task?.failureStage, - task?.failure_detail || task?.failureDetail, - task?.error_message || task?.errorMessage, - ].map((value) => String(value ?? '').trim()).join('|'); - } - - function shouldWatchGpcTaskProgress(task = {}, state = {}) { - if (!task || isGpcTaskTerminal(task.status)) { - return false; - } - if (isGpcTaskOtpWait(task, state) || isGpcTaskPinWait(task, state)) { - return false; - } - return true; - } - - function getGpcTaskStaleStatusTimeoutMs(state = {}) { - const configuredSeconds = Number(state?.gopayHelperTaskStaleSeconds); - if (Number.isFinite(configuredSeconds) && configuredSeconds > 0) { - return Math.max(15000, Math.min(600000, Math.floor(configuredSeconds * 1000))); - } - return GPC_TASK_STALE_STATUS_TIMEOUT_MS; - } - - function buildGpcTaskStaleStatusError(task = {}, staleTimeoutMs = GPC_TASK_STALE_STATUS_TIMEOUT_MS) { - const seconds = Math.max(1, Math.round(staleTimeoutMs / 1000)); - const label = formatGpcRemoteStageLabel(task?.remote_stage) - || task?.status_text - || task?.status - || '未知状态'; - return new Error(`GPC_TASK_ENDED::GPC 任务状态超过 ${seconds} 秒无进展(${label}),请重新创建任务。`); - } - - 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, - gopayHelperTaskProgressSignature: '', - gopayHelperTaskProgressAt: 0, - gopayHelperTaskProgressTaskId: '', - 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, + button.scrollIntoView?.({ block: 'center', inline: 'center' }); + button.click?.(); + return { + clicked: true, + buttonText: textOf(button), + }; + }, }); + return results?.[0]?.result || {}; } async function executeGpcHelperBilling(state = {}) { - const taskId = String(state?.gopayHelperTaskId || '').trim(); - const apiUrl = normalizeGpcHelperBaseUrl(state?.gopayHelperApiUrl || ''); - 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; - let lastProgressSignature = String(state?.gopayHelperTaskProgressSignature || '').trim(); - let lastProgressAt = normalizeEpochMilliseconds(state?.gopayHelperTaskProgressAt || 0) || Date.now(); - let lastProgressTaskId = String(state?.gopayHelperTaskProgressTaskId || '').trim(); - if (lastProgressTaskId !== taskId) { - lastProgressSignature = ''; - lastProgressAt = Date.now(); - lastProgressTaskId = ''; + const tabId = await getGpcPortalTabId(state); + if (chrome?.tabs?.update) { + await chrome.tabs.update(tabId, { url: GPC_PORTAL_URL, active: true }).catch(() => ( + chrome.tabs.update(tabId, { active: true }).catch(() => {}) + )); } - const staleStatusTimeoutMs = getGpcTaskStaleStatusTimeoutMs(state); + await addLog('步骤 7:正在等待 GPC 页面加载完成...', 'info'); + await waitForTabCompleteUntilStopped(tabId); + await sleepWithStop(800); - if (!taskId) { - throw new Error('步骤 7:GPC 模式缺少 task_id,请先执行步骤 6。'); - } - if (!apiUrl) { - throw new Error('步骤 7:GPC 模式缺少 API 地址。'); - } - if (!apiKey) { - throw new Error('步骤 7:GPC 模式缺少 API Key。'); - } + const deadline = Date.now() + getGpcPageTimeoutMs(state); + let startAttempts = 0; + let lastSignature = ''; + let hasClickedStart = false; - const configuredPhoneMode = normalizeGpcHelperPhoneMode(state?.gopayHelperPhoneMode || state?.phoneMode || GPC_HELPER_PHONE_MODE_MANUAL); - const rawPin = String(state?.gopayHelperPin || '').trim(); - const pinDigits = rawPin.replace(/[^\d]/g, ''); - const pin = normalizeSixDigitPin(rawPin); - if (configuredPhoneMode === GPC_HELPER_PHONE_MODE_MANUAL && !pin) { - if (taskId && apiUrl && apiKey) { - await stopGpcTaskBestEffort(apiUrl, taskId, apiKey, 'PIN 配置错误'); + while (Date.now() <= deadline) { + throwIfStopped(); + const pageState = await inspectGpcPortalPage(tabId); + const buttonText = normalizeText(pageState.startButtonText); + const logText = String(pageState.logText || pageState.bodyText || ''); + const signature = `${buttonText}|${pageState.hasSubscriptionDone ? 'done' : ''}|${pageState.noTrial ? 'no-trial' : ''}|${logText.slice(-300)}`; + + if (signature && signature !== lastSignature) { + lastSignature = signature; + await addLog( + `步骤 7:GPC 页面状态:${buttonText || '未识别按钮'}${pageState.hasSubscriptionDone ? ' / 订阅完成' : ''}`, + pageState.hasSubscriptionDone ? 'ok' : 'info' + ); } - throw new Error(pinDigits - ? '步骤 7:GPC PIN 必须是 6 位数字,请检查侧边栏配置。' - : '步骤 7:GPC 手动模式缺少 PIN 配置。'); - } - await addLog(`步骤 7:GPC ${configuredPhoneMode === GPC_HELPER_PHONE_MODE_AUTO ? '自动' : '手动'}模式开始轮询任务(task_id: ${taskId})...`, 'info'); - try { - while (Date.now() <= deadline) { - throwIfStopped(); - const task = await fetchGpcTaskStatus(apiUrl, taskId, apiKey); - await addLog(formatGpcTaskStatusLog(task), 'info'); + if (pageState.noTrial) { + throw new Error('PLUS_CHECKOUT_NON_FREE_TRIAL::步骤 7:该账户没有试用资格,当前轮 GPC 充值失败。'); + } - if (task.status === 'completed') { - terminalReached = true; - await setState({ - plusCheckoutSource: PLUS_PAYMENT_METHOD_GPC_HELPER, - }); - await addLog('步骤 7:GPC 任务已完成,准备继续下一步。', 'ok'); - await completeNodeFromBackground('plus-checkout-billing', { - plusCheckoutSource: PLUS_PAYMENT_METHOD_GPC_HELPER, - }); - return; + if (pageState.hasSubscriptionDone && /开始\s*Plus\s*充值/.test(buttonText)) { + await setState({ + plusCheckoutSource: PLUS_PAYMENT_METHOD_GPC_HELPER, + gpcPageStatus: 'completed', + gpcPageStatusText: '订阅完成', + }); + await addLog('步骤 7:GPC 页面显示订阅完成,准备继续下一步。', 'ok'); + await completeNodeFromBackground('plus-checkout-billing', { + plusCheckoutSource: PLUS_PAYMENT_METHOD_GPC_HELPER, + }); + return; + } + + if (/任务进行中/.test(buttonText)) { + await sleepWithStop(GPC_PAGE_POLL_INTERVAL_MS); + continue; + } + + if (/开始\s*Plus\s*充值/.test(buttonText) && !pageState.startButtonDisabled) { + if (startAttempts >= GPC_PAGE_MAX_START_ATTEMPTS) { + throw new Error(`GPC_PAGE_FLOW_ENDED::步骤 7:GPC 页面已尝试启动 ${startAttempts} 次仍未显示订阅完成。`); } - - if (['failed', 'expired', 'discarded'].includes(task.status)) { - terminalReached = true; - throw buildGpcTaskEndedError(task, 'GPC 任务已结束,请重新创建任务。'); - } - - if (shouldWatchGpcTaskProgress(task, state)) { - const progressSignature = buildGpcTaskProgressSignature(task); - const now = Date.now(); - if (progressSignature && (progressSignature !== lastProgressSignature || lastProgressTaskId !== taskId)) { - lastProgressSignature = progressSignature; - lastProgressAt = now; - lastProgressTaskId = taskId; - await setState({ - gopayHelperTaskProgressSignature: progressSignature, - gopayHelperTaskProgressAt: now, - gopayHelperTaskProgressTaskId: taskId, - }); - } else if (progressSignature && now - lastProgressAt >= staleStatusTimeoutMs) { - throw buildGpcTaskStaleStatusError(task, staleStatusTimeoutMs); - } + if (hasClickedStart && !pageState.hasSubscriptionDone) { + await addLog('步骤 7:GPC 页面回到开始状态但未显示订阅完成,准备再次启动。', 'warn'); } else { - lastProgressSignature = ''; - lastProgressAt = Date.now(); - lastProgressTaskId = ''; - await setState({ - gopayHelperTaskProgressSignature: '', - gopayHelperTaskProgressAt: 0, - gopayHelperTaskProgressTaskId: taskId, - }); + await addLog('步骤 7:正在点击“开始 Plus 充值”。', 'info'); } - - if (isGpcTaskOtpWait(task, state)) { - 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, state) && !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'); + const clickResult = await clickGpcStartButton(tabId); + if (!clickResult.clicked) { + await addLog(`步骤 7:暂时无法点击开始按钮(${clickResult.reason || '未知原因'}),继续等待。`, 'warn'); + await sleepWithStop(GPC_PAGE_POLL_INTERVAL_MS); + continue; } + startAttempts += 1; + hasClickedStart = true; + await setState({ + plusCheckoutSource: PLUS_PAYMENT_METHOD_GPC_HELPER, + gpcPageStatus: 'running', + gpcPageStatusText: `页面启动第 ${startAttempts} 次`, + }); + await sleepWithStop(GPC_PAGE_POLL_INTERVAL_MS); + continue; + } - 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 sleepWithStop(GPC_PAGE_POLL_INTERVAL_MS); } + + throw new Error('GPC_PAGE_FLOW_ENDED::步骤 7:GPC 页面等待超时,未检测到订阅完成。'); } function resolveMeiguodizhiCountryCode(value = '') { diff --git a/flows/openai/workflow.js b/flows/openai/workflow.js index afd35ef..de98426 100644 --- a/flows/openai/workflow.js +++ b/flows/openai/workflow.js @@ -2479,7 +2479,7 @@ "id": 6, "order": 60, "key": "plus-checkout-create", - "title": "创建 GPC 订单", + "title": "打开 GPC 页面并准备", "sourceId": "plus-checkout", "driverId": "flows/openai/content/plus-checkout", "command": "plus-checkout-create", @@ -2489,7 +2489,7 @@ "id": 7, "order": 70, "key": "plus-checkout-billing", - "title": "等待 GPC 任务完成", + "title": "启动并等待 GPC 完成", "sourceId": "plus-checkout", "driverId": "flows/openai/content/plus-checkout", "command": "plus-checkout-billing", @@ -2603,7 +2603,7 @@ "id": 6, "order": 60, "key": "plus-checkout-create", - "title": "创建 GPC 订单", + "title": "打开 GPC 页面并准备", "sourceId": "plus-checkout", "driverId": "flows/openai/content/plus-checkout", "command": "plus-checkout-create", @@ -2613,7 +2613,7 @@ "id": 7, "order": 70, "key": "plus-checkout-billing", - "title": "等待 GPC 任务完成", + "title": "启动并等待 GPC 完成", "sourceId": "plus-checkout", "driverId": "flows/openai/content/plus-checkout", "command": "plus-checkout-billing", @@ -2686,7 +2686,7 @@ "id": 6, "order": 60, "key": "plus-checkout-create", - "title": "创建 GPC 订单", + "title": "打开 GPC 页面并准备", "sourceId": "plus-checkout", "driverId": "flows/openai/content/plus-checkout", "command": "plus-checkout-create", @@ -2696,7 +2696,7 @@ "id": 7, "order": 70, "key": "plus-checkout-billing", - "title": "等待 GPC 任务完成", + "title": "启动并等待 GPC 完成", "sourceId": "plus-checkout", "driverId": "flows/openai/content/plus-checkout", "command": "plus-checkout-billing", @@ -2769,7 +2769,7 @@ "id": 6, "order": 60, "key": "plus-checkout-create", - "title": "创建 GPC 订单", + "title": "打开 GPC 页面并准备", "sourceId": "plus-checkout", "driverId": "flows/openai/content/plus-checkout", "command": "plus-checkout-create", @@ -2779,7 +2779,7 @@ "id": 7, "order": 70, "key": "plus-checkout-billing", - "title": "等待 GPC 任务完成", + "title": "启动并等待 GPC 完成", "sourceId": "plus-checkout", "driverId": "flows/openai/content/plus-checkout", "command": "plus-checkout-billing", @@ -2904,7 +2904,7 @@ "id": 6, "order": 60, "key": "plus-checkout-create", - "title": "创建 GPC 订单", + "title": "打开 GPC 页面并准备", "sourceId": "plus-checkout", "driverId": "flows/openai/content/plus-checkout", "command": "plus-checkout-create", @@ -2914,7 +2914,7 @@ "id": 7, "order": 70, "key": "plus-checkout-billing", - "title": "等待 GPC 任务完成", + "title": "启动并等待 GPC 完成", "sourceId": "plus-checkout", "driverId": "flows/openai/content/plus-checkout", "command": "plus-checkout-billing", diff --git a/gopay-utils.js b/gopay-utils.js index 3622067..6fa7466 100644 --- a/gopay-utils.js +++ b/gopay-utils.js @@ -6,10 +6,8 @@ const PLUS_PAYMENT_METHOD_NONE = 'none'; const PLUS_PAYMENT_METHOD_GOPAY = 'gopay'; const PLUS_PAYMENT_METHOD_GPC_HELPER = 'gpc-helper'; - const DEFAULT_GPC_HELPER_API_URL = 'https://gpc.qlhazycoder.top'; - const GPC_HELPER_PHONE_MODE_AUTO = 'auto'; - const GPC_HELPER_PHONE_MODE_MANUAL = 'manual'; - const ALLOWED_GPC_HELPER_REMOTE_HOST = 'gpc.qlhazycoder.top'; + const DEFAULT_GPC_BASE_URL = 'https://gpc.qlhazycoder.top'; + const ALLOWED_GPC_REMOTE_HOST = 'gpc.qlhazycoder.top'; function normalizePlusPaymentMethod(value = '') { const normalized = String(value || '').trim().toLowerCase(); @@ -57,13 +55,6 @@ return String(value || '').trim().replace(/[^\d]/g, ''); } - function normalizeGpcHelperPhoneMode(value = '') { - const normalized = String(value || '').trim().toLowerCase(); - return normalized === GPC_HELPER_PHONE_MODE_AUTO || normalized === 'builtin' - ? GPC_HELPER_PHONE_MODE_AUTO - : GPC_HELPER_PHONE_MODE_MANUAL; - } - function normalizeGpcRemainingUses(value) { if (value === undefined || value === null || String(value).trim() === '') { return null; @@ -72,6 +63,14 @@ return Number.isFinite(numeric) ? Math.max(0, Math.floor(numeric)) : null; } + function normalizeGpcCardKey(value = '') { + return String(value || '').trim().toUpperCase(); + } + + function isGpcCardKeyFormat(value = '') { + return /^GPC-[A-F0-9]{8}-[A-F0-9]{8}-[A-F0-9]{8}$/.test(normalizeGpcCardKey(value)); + } + function unwrapGpcBalancePayload(payload = {}) { const data = unwrapGpcResponse(payload); if (!data || typeof data !== 'object' || Array.isArray(data)) { @@ -85,13 +84,13 @@ 'uses', 'available_uses', 'availableUses', - 'auto_mode_enabled', - 'autoModeEnabled', - 'auto_enabled', - 'autoEnabled', 'status', 'card_status', 'cardStatus', + 'card_type', + 'cardType', + 'expires_at', + 'expiresAt', ].some((key) => Object.prototype.hasOwnProperty.call(data, key)); if (!hasBalanceFields && data.data && typeof data.data === 'object' && !Array.isArray(data.data)) { return data.data; @@ -115,20 +114,7 @@ ); } - function isGpcAutoModeEnabled(payload = {}) { - const data = unwrapGpcBalancePayload(payload); - if (!data || typeof data !== 'object') { - return false; - } - const raw = data.auto_mode_enabled ?? data.autoModeEnabled ?? data.auto_enabled ?? data.autoEnabled; - if (typeof raw === 'boolean') { - return raw; - } - const normalized = String(raw ?? '').trim().toLowerCase(); - return normalized === 'true' || normalized === '1' || normalized === 'yes' || normalized === 'enabled'; - } - - function getGpcApiKeyStatus(payload = {}) { + function getGpcCardStatus(payload = {}) { const data = unwrapGpcBalancePayload(payload); if (!data || typeof data !== 'object') { return ''; @@ -136,41 +122,30 @@ return String(data.status || data.card_status || data.cardStatus || '').trim(); } - function normalizeGpcOtpChannel(value = '') { - const normalized = String(value || '').trim().toLowerCase(); - if (normalized === 'sms') { - return 'sms'; - } - return 'whatsapp'; - } - - function normalizeGpcHelperBaseUrl(apiUrl = '') { - let normalized = String(apiUrl || DEFAULT_GPC_HELPER_API_URL).trim(); + function normalizeGpcBaseUrl(apiUrl = '') { + let normalized = String(apiUrl || DEFAULT_GPC_BASE_URL).trim(); if (!normalized) { - return DEFAULT_GPC_HELPER_API_URL; + return DEFAULT_GPC_BASE_URL; } 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\/web\/card\/balance(?:\?.*)?$/i, ''); normalized = normalized.replace(/\/api\/card\/balance(?:\?.*)?$/i, ''); - normalized = normalized.replace(/\/api\/card\/redeem-api-key(?:\?.*)?$/i, ''); try { const parsed = new URL(normalized); const hostname = parsed.hostname.toLowerCase(); - if (hostname === ALLOWED_GPC_HELPER_REMOTE_HOST || hostname === 'localhost' || hostname === '127.0.0.1') { - return normalized || DEFAULT_GPC_HELPER_API_URL; + if (hostname === ALLOWED_GPC_REMOTE_HOST || hostname === 'localhost' || hostname === '127.0.0.1') { + return normalized || DEFAULT_GPC_BASE_URL; } - return DEFAULT_GPC_HELPER_API_URL; + return DEFAULT_GPC_BASE_URL; } catch { - return DEFAULT_GPC_HELPER_API_URL; + return DEFAULT_GPC_BASE_URL; } } - function buildGpcHelperApiUrl(apiUrl = '', path = '') { - const baseUrl = normalizeGpcHelperBaseUrl(apiUrl); + function buildGpcApiUrl(apiUrl = '', path = '') { + const baseUrl = normalizeGpcBaseUrl(apiUrl); if (!baseUrl) { return ''; } @@ -178,42 +153,13 @@ return `${baseUrl}${normalizedPath}`; } - 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; + function buildGpcCardBalanceUrl(apiUrl = '', cardKey = '') { + const baseUrl = buildGpcApiUrl(apiUrl, '/api/web/card/balance'); + const normalizedCardKey = normalizeGpcCardKey(cardKey); + if (!baseUrl || !normalizedCardKey) { + return baseUrl; } - 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}`); + return `${baseUrl}?card_key=${encodeURIComponent(normalizedCardKey)}`; } function unwrapGpcResponse(payload = {}) { @@ -329,56 +275,6 @@ return status ? `HTTP ${status}` : '未知错误'; } - function buildGpcOtpPayload(input = {}) { - const payload = { - reference_id: String(input.reference_id ?? input.referenceId ?? '').trim(), - otp: normalizeGoPayOtp(input.otp ?? input.code ?? ''), - }; - 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 ?? ''), - }; - 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 buildGpcTaskOtpPayload(input = {}) { - return { - otp: normalizeGoPayOtp(input.otp ?? input.code ?? ''), - }; - } - - function buildGpcTaskPinPayload(input = {}) { - return { - pin: normalizeGoPayPin(input.pin ?? ''), - }; - } - function formatGpcBalancePayload(payload = {}) { const data = unwrapGpcBalancePayload(payload); if (!data || typeof data !== 'object') { @@ -418,43 +314,28 @@ return { DEFAULT_GOPAY_COUNTRY_CODE, - DEFAULT_GPC_HELPER_API_URL, - GPC_HELPER_PHONE_MODE_AUTO, - GPC_HELPER_PHONE_MODE_MANUAL, + DEFAULT_GPC_BASE_URL, PLUS_PAYMENT_METHOD_GPC_HELPER, PLUS_PAYMENT_METHOD_GOPAY, PLUS_PAYMENT_METHOD_NONE, PLUS_PAYMENT_METHOD_PAYPAL, PLUS_PAYMENT_METHOD_PAYPAL_HOSTED, buildGpcCardBalanceUrl, - buildGpcApiKeyBalanceUrl, - buildGpcApiKeyHeaders, - buildGpcHelperApiUrl, - buildGpcOtpPayload, - buildGpcOtpRetryPayload, - buildGpcPinPayload, - buildGpcPinRetryPayload, - buildGpcTaskActionUrl, - buildGpcTaskCreateUrl, - buildGpcTaskOtpPayload, - buildGpcTaskPinPayload, - buildGpcTaskQueryUrl, + buildGpcApiUrl, extractGpcResponseErrorDetail, formatGpcBalancePayload, - getGpcApiKeyStatus, + getGpcCardStatus, getGpcBalanceRemainingUses, isGpcUnifiedResponseOk, - isGpcAutoModeEnabled, - normalizeGpcHelperBaseUrl, - normalizeGpcHelperPhoneMode, + isGpcCardKeyFormat, + normalizeGpcBaseUrl, + normalizeGpcCardKey, normalizeGpcRemainingUses, - normalizeGpcTaskId, normalizeGoPayCountryCode, normalizeGoPayPhone, normalizeGoPayPhoneForCountry, normalizeGoPayOtp, normalizeGoPayPin, - normalizeGpcOtpChannel, normalizePlusPaymentMethod, unwrapGpcBalancePayload, unwrapGpcResponse, diff --git a/scripts/gpc_sms_helper_macos.py b/scripts/gpc_sms_helper_macos.py deleted file mode 100644 index f02a317..0000000 --- a/scripts/gpc_sms_helper_macos.py +++ /dev/null @@ -1,424 +0,0 @@ -#!/usr/bin/env python3 -"""Local macOS SMS OTP helper for GPC GoPay flows. - -Run this on the Mac that receives forwarded iPhone SMS messages. The helper -reads a copy of the macOS Messages database and exposes the latest matching OTP -on a localhost HTTP endpoint for the Chrome extension. -""" - -from __future__ import annotations - -import argparse -import datetime as dt -import json -import os -import re -import shutil -import sqlite3 -import sys -import tempfile -import threading -import time -from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer -from pathlib import Path -from typing import Optional -from urllib.parse import parse_qs, urlparse - -HOST = "127.0.0.1" -PORT = 18767 -DEFAULT_DB = "~/Library/Messages/chat.db" -MAC_ABSOLUTE_EPOCH = dt.datetime(2001, 1, 1, tzinfo=dt.timezone.utc) - -OTP_PATTERNS = ( - re.compile(r"(?i)\bOTP\s*[::]?\s*([0-9]{4,8})\b"), - re.compile(r"#([0-9]{4,8})\b"), - re.compile(r"(? bool: - override = os.environ.get("GPC_SMS_HELPER_ALLOW_NON_MAC", "").strip().lower() - return sys.platform == "darwin" or override in {"1", "true", "yes"} - - -def require_macos() -> None: - if not is_macos(): - raise RuntimeError( - "GPC 本地 SMS Helper 仅支持 macOS:需要读取 ~/Library/Messages/chat.db。" - "请确认接收验证码的 iPhone 已开启短信转发,并能在 Mac 信息 app 中看到短信。" - ) - - -def extract_gopay_otp(text: str, require_keywords: bool = True) -> Optional[str]: - raw = text or "" - lowered = raw.lower() - if require_keywords and not any(keyword in lowered for keyword in KEYWORDS): - return None - for pattern in OTP_PATTERNS: - match = pattern.search(raw) - if match: - return match.group(1) - return None - - -def normalize_phone_key(value: object) -> str: - digits = re.sub(r"\D+", "", str(value or "")) - return f"+{digits}" if digits else "" - - -def mac_message_time_to_datetime(value: int | float | None) -> dt.datetime: - if not value: - return dt.datetime.now(dt.timezone.utc) - seconds = float(value) - if seconds > 10_000_000_000: - seconds = seconds / 1_000_000_000 - return MAC_ABSOLUTE_EPOCH + dt.timedelta(seconds=seconds) - - -def update_state(**updates: object) -> None: - with STATE_LOCK: - STATE.update(updates) - - -def get_state() -> dict: - with STATE_LOCK: - return dict(STATE) - - -def copy_messages_db(db_path: Path) -> Path: - if not db_path.exists(): - raise FileNotFoundError(f"Messages 数据库不存在:{db_path}") - tmpdir = Path(tempfile.mkdtemp(prefix="gpc_messages_")) - copied = tmpdir / "chat.db" - shutil.copy2(db_path, copied) - wal = db_path.with_name(db_path.name + "-wal") - shm = db_path.with_name(db_path.name + "-shm") - if wal.exists(): - shutil.copy2(wal, copied.with_name("chat.db-wal")) - if shm.exists(): - shutil.copy2(shm, copied.with_name("chat.db-shm")) - return copied - - -def read_recent_messages(db_path: Path, after_rowid: int = 0, limit: int = 80) -> list[dict]: - copied = copy_messages_db(db_path) - try: - conn = sqlite3.connect(str(copied)) - conn.row_factory = sqlite3.Row - params = (max(0, int(after_rowid or 0)), max(1, int(limit or 80))) - try: - rows = conn.execute( - """ - SELECT - message.ROWID AS rowid, - message.guid AS guid, - message.text AS text, - message.date AS date, - message.service AS service, - message.destination_caller_id AS destination_caller_id, - message.account AS account, - handle.id AS handle, - chat.last_addressed_handle AS last_addressed_handle, - chat.account_login AS account_login - FROM message - LEFT JOIN handle ON message.handle_id = handle.ROWID - LEFT JOIN chat_message_join ON chat_message_join.message_id = message.ROWID - LEFT JOIN chat ON chat.ROWID = chat_message_join.chat_id - WHERE message.ROWID > ? - AND message.text IS NOT NULL - ORDER BY message.ROWID DESC - LIMIT ? - """, - params, - ).fetchall() - except sqlite3.OperationalError: - rows = conn.execute( - """ - SELECT - message.ROWID AS rowid, - message.guid AS guid, - message.text AS text, - message.date AS date, - message.service AS service, - message.account AS account, - handle.id AS handle - FROM message - LEFT JOIN handle ON message.handle_id = handle.ROWID - WHERE message.ROWID > ? - AND message.text IS NOT NULL - ORDER BY message.ROWID DESC - LIMIT ? - """, - params, - ).fetchall() - return [dict(row) for row in rows] - finally: - try: - conn.close() # type: ignore[name-defined] - except Exception: - pass - shutil.rmtree(copied.parent, ignore_errors=True) - - -def get_record_phone(row: dict) -> str: - for key in ("destination_caller_id", "last_addressed_handle", "account_login", "account"): - phone = normalize_phone_key(row.get(key)) - if phone: - return phone - return "" - - -def make_otp_record(row: dict, otp: str) -> dict: - received_at = mac_message_time_to_datetime(row.get("date")).isoformat() - phone_e164 = get_record_phone(row) - return { - "otp": otp, - "code": otp, - "message_id": str(row.get("guid") or row.get("rowid") or ""), - "rowid": int(row.get("rowid") or 0), - "sender": str(row.get("handle") or ""), - "service": str(row.get("service") or ""), - "account_phone": phone_e164, - "phone_e164": phone_e164, - "received_at": received_at, - "message_text": str(row.get("text") or ""), - } - - -def append_otp(record: dict, max_records: int = 30) -> None: - with STATE_LOCK: - records = [item for item in STATE.get("otps", []) if item.get("message_id") != record.get("message_id")] - records.insert(0, record) - STATE["otps"] = records[:max_records] - STATE["last_otp"] = record - STATE["last_rowid"] = max(int(STATE.get("last_rowid") or 0), int(record.get("rowid") or 0)) - - -def parse_timestamp_ms(value: object) -> int: - if value is None: - return 0 - if isinstance(value, (int, float)): - numeric = float(value) - else: - raw = str(value).strip() - if not raw: - return 0 - try: - numeric = float(raw) - except ValueError: - try: - parsed = dt.datetime.fromisoformat(raw.replace("Z", "+00:00")) - if parsed.tzinfo is None: - parsed = parsed.replace(tzinfo=dt.timezone.utc) - return int(parsed.timestamp() * 1000) - except ValueError: - return 0 - if numeric <= 0: - return 0 - if numeric < 100_000_000_000: - numeric *= 1000 - return int(numeric) - - -def record_matches_phone(record: dict, phone: str = "") -> bool: - wanted = normalize_phone_key(phone) - if not wanted: - return True - return normalize_phone_key(record.get("phone_e164") or record.get("account_phone")) == wanted - - -def select_otp_record(state: dict, after_ms: int = 0, phone: str = "") -> Optional[dict]: - records = state.get("otps") - if not isinstance(records, list): - records = [] - if after_ms > 0: - for record in records: - if ( - isinstance(record, dict) - and record_matches_phone(record, phone) - and parse_timestamp_ms(record.get("received_at")) >= after_ms - ): - return record - return None - record = state.get("last_otp") or None - if isinstance(record, dict) and record_matches_phone(record, phone): - return record - for record in records: - if isinstance(record, dict) and record_matches_phone(record, phone): - return record - return None - - -def consume_otp_record(phone: str = "", record: Optional[dict] = None) -> None: - wanted = normalize_phone_key(phone) - consumed_message_id = str((record or {}).get("message_id") or "").strip() - consumed_rowid = int((record or {}).get("rowid") or 0) - - def is_consumed_record(item: object) -> bool: - if not isinstance(item, dict): - return False - if consumed_message_id and str(item.get("message_id") or "").strip() == consumed_message_id: - return True - if consumed_rowid and int(item.get("rowid") or 0) == consumed_rowid: - return True - return False - - def is_same_record(left: object, right: object) -> bool: - if not isinstance(left, dict) or not isinstance(right, dict): - return False - left_message_id = str(left.get("message_id") or "").strip() - right_message_id = str(right.get("message_id") or "").strip() - if left_message_id and right_message_id and left_message_id == right_message_id: - return True - left_rowid = int(left.get("rowid") or 0) - right_rowid = int(right.get("rowid") or 0) - if left_rowid > 0 and right_rowid > 0 and left_rowid == right_rowid: - return True - return left == right - - with STATE_LOCK: - records = STATE.get("otps") - if not isinstance(records, list): - records = [] - - if consumed_message_id or consumed_rowid: - next_records = [item for item in records if isinstance(item, dict) and not is_consumed_record(item)] - elif wanted: - removed_once = False - next_records = [] - for item in records: - if not isinstance(item, dict): - continue - if not removed_once and record_matches_phone(item, wanted): - removed_once = True - continue - next_records.append(item) - else: - next_records = [] - - STATE["otps"] = next_records - last_otp = STATE.get("last_otp") - if isinstance(last_otp, dict) and not any(is_same_record(last_otp, item) for item in STATE["otps"]): - STATE["last_otp"] = STATE["otps"][0] if STATE["otps"] else None - - -def scan_once(db_path: Path, require_keywords: bool = True) -> None: - state = get_state() - after_rowid = int(state.get("last_rowid") or 0) - rows = read_recent_messages(db_path, after_rowid=after_rowid) - max_rowid = after_rowid - for row in reversed(rows): - max_rowid = max(max_rowid, int(row.get("rowid") or 0)) - otp = extract_gopay_otp(row.get("text") or "", require_keywords=require_keywords) - if otp: - record = make_otp_record(row, otp) - append_otp(record) - print(f"captured OTP {otp} from message {record['message_id']} at {record['received_at']}", flush=True) - update_state(last_rowid=max_rowid, last_scan_at=dt.datetime.now(dt.timezone.utc).isoformat(), last_error="") - - -def scan_loop(db_path: Path, interval_seconds: float, require_keywords: bool) -> None: - update_state(db_path=str(db_path)) - while True: - try: - scan_once(db_path, require_keywords=require_keywords) - except Exception as exc: - update_state(last_error=str(exc), last_scan_at=dt.datetime.now(dt.timezone.utc).isoformat()) - print(f"scan error: {exc}", file=sys.stderr, flush=True) - time.sleep(max(0.5, float(interval_seconds or 2))) - - -def write_json(handler: BaseHTTPRequestHandler, status: int, payload: dict) -> None: - body = json.dumps(payload, ensure_ascii=False).encode("utf-8") - handler.send_response(status) - handler.send_header("Content-Type", "application/json; charset=utf-8") - handler.send_header("Access-Control-Allow-Origin", "*") - handler.send_header("Cache-Control", "no-store") - handler.send_header("Content-Length", str(len(body))) - handler.end_headers() - handler.wfile.write(body) - - -class HelperHandler(BaseHTTPRequestHandler): - server_version = "GpcSmsHelper/1.0" - - def log_message(self, fmt: str, *args: object) -> None: - print(f"{self.address_string()} - {fmt % args}", flush=True) - - def do_GET(self) -> None: # noqa: N802 - parsed = urlparse(self.path) - if parsed.path == "/health": - state = get_state() - write_json(self, 200, { - "ok": True, - "status": "ok", - "has_otp": bool(state.get("last_otp")), - "last_scan_at": state.get("last_scan_at"), - "last_error": state.get("last_error"), - }) - return - if parsed.path in ("/otp", "/latest-otp"): - query = parse_qs(parsed.query) - consume = str(query.get("consume", ["0"])[0]).strip().lower() in {"1", "true", "yes"} - after_ms = parse_timestamp_ms(query.get("after_ms", query.get("after", ["0"]))[0]) - phone = str((query.get("phone") or query.get("phone_e164") or query.get("phone_number") or [""])[0]).strip() - state = get_state() - record = select_otp_record(state, after_ms=after_ms, phone=phone) - if not record: - write_json(self, 200, {"ok": True, "otp": "", "code": "", "status": "waiting", "message": "未查询到验证码"}) - return - payload = {"ok": True, "status": "found", **record} - if consume: - consume_otp_record(phone=phone, record=record) - write_json(self, 200, payload) - return - write_json(self, 404, {"ok": False, "error": "not_found"}) - - -def parse_args() -> argparse.Namespace: - parser = argparse.ArgumentParser(description="Run a local macOS Messages HTTP helper for GPC SMS OTP.") - parser.add_argument("--host", default=HOST, help="Bind host, default 127.0.0.1") - parser.add_argument("--port", type=int, default=PORT, help="Bind port, default 18767") - parser.add_argument("--db", default=DEFAULT_DB, help="Path to macOS Messages chat.db") - parser.add_argument("--interval", type=float, default=2.0, help="Message scan interval in seconds") - parser.add_argument("--no-keywords", action="store_true", help="Accept any numeric OTP without GPC/OpenAI keywords") - return parser.parse_args() - - -def main() -> int: - args = parse_args() - try: - require_macos() - db_path = Path(os.path.expanduser(args.db)).resolve() - scanner = threading.Thread( - target=scan_loop, - args=(db_path, args.interval, not args.no_keywords), - daemon=True, - ) - scanner.start() - server = ThreadingHTTPServer((args.host, int(args.port)), HelperHandler) - print(f"GPC SMS Helper listening on http://{args.host}:{int(args.port)}", flush=True) - print("请确认 iPhone 短信已转发到本机 Messages。", flush=True) - server.serve_forever() - return 0 - except KeyboardInterrupt: - return 0 - except Exception as exc: - print(str(exc), file=sys.stderr, flush=True) - return 1 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/sidepanel/sidepanel.css b/sidepanel/sidepanel.css index 1d49a5c..316ef78 100644 --- a/sidepanel/sidepanel.css +++ b/sidepanel/sidepanel.css @@ -921,6 +921,16 @@ header { min-width: 0; } +.gpc-card-key-inline > .input-with-icon { + flex: 0 1 376px; + max-width: 376px; +} + +.gpc-card-key-inline > .data-value { + flex: 0 0 auto; + white-space: nowrap; +} + .ip-proxy-fold-row { display: block; } @@ -1287,6 +1297,18 @@ header { min-width: 0; } +.data-value[data-tone="ok"] { + color: var(--green); +} + +.data-value[data-tone="running"] { + color: var(--blue); +} + +.data-value[data-tone="error"] { + color: var(--red); +} + .data-value-fill { flex: 1; } @@ -2094,10 +2116,6 @@ header { cursor: default; } -.gpc-helper-api-input { - font-family: 'JetBrains Mono', monospace; -} - .editable-list-picker { position: relative; flex: 1 1 0; diff --git a/sidepanel/sidepanel.html b/sidepanel/sidepanel.html index 61031a3..42c7ab1 100644 --- a/sidepanel/sidepanel.html +++ b/sidepanel/sidepanel.html @@ -337,8 +337,8 @@ + PayPal 订阅链路 @@ -377,90 +377,18 @@ 支付成功页出现后再继续账号接入 -
-