From 27cbf2a61b7c65262d7b3d9becdb9d4ced139423 Mon Sep 17 00:00:00 2001 From: QLHazyCoder <2825305047@qq.com> Date: Fri, 29 May 2026 14:58:32 +0800 Subject: [PATCH] fix: validate GPC card key before session fill --- .../background/steps/create-plus-checkout.js | 103 +++++++++++++----- tests/plus-checkout-create-wait.test.js | 24 +++- 2 files changed, 96 insertions(+), 31 deletions(-) diff --git a/flows/openai/background/steps/create-plus-checkout.js b/flows/openai/background/steps/create-plus-checkout.js index 9a011c2..2cd1bac 100644 --- a/flows/openai/background/steps/create-plus-checkout.js +++ b/flows/openai/background/steps/create-plus-checkout.js @@ -1282,6 +1282,21 @@ function FindProxyForURL(url, host) { return cardKey; } + function resolveGpcCardKeySegments(cardKey = '') { + const text = String(cardKey || '').trim().toUpperCase(); + const compact = text.replace(/\s+/g, ''); + const rawSegments = compact.includes('-') || compact.includes('_') + ? compact.split(/[-_]+/).filter(Boolean) + : (compact.replace(/^GPC[-_]?/i, '').match(/.{1,8}/g) || []); + const withoutPrefix = rawSegments[0] === 'GPC' ? rawSegments.slice(1) : rawSegments; + const segments = withoutPrefix.slice(0, 3) + .map((segment) => String(segment || '').replace(/[^A-Z0-9]/g, '').toUpperCase()); + if (segments.length !== 3 || segments.some((segment) => !/^[A-Z0-9]{8}$/.test(segment))) { + throw new Error('步骤 6:GPC 卡密格式不正确,应为 GPC-XXXXXXXX-XXXXXXXX-XXXXXXXX。'); + } + return segments; + } + function buildGpcPortalUrl(state = {}) { const baseUrl = normalizeGpcBaseUrl(state?.gpcBaseUrl || DEFAULT_GPC_BASE_URL) .replace(/\/+$/g, ''); @@ -1391,7 +1406,7 @@ function FindProxyForURL(url, host) { return session; } - async function prepareGpcPortalPage(tabId, cardKey = '', sessionJson = '') { + async function prepareGpcCardKeyOnPortalPage(tabId, cardSegments = []) { if (!chrome?.scripting?.executeScript) { throw new Error('步骤 6:当前运行环境不支持脚本注入,无法填写 GPC 页面。'); } @@ -1507,7 +1522,7 @@ function FindProxyForURL(url, host) { const results = await chrome.scripting.executeScript({ target: { tabId }, - func: (rawCardKey, rawSessionJson) => { + func: (rawCardSegments) => { const textOf = (element) => String(element?.innerText || element?.textContent || element?.value || '').replace(/\s+/g, ' ').trim(); const isVisible = (element) => { if (!element) return false; @@ -1541,13 +1556,9 @@ function FindProxyForURL(url, host) { const cardModeButton = modeButtons.find((element) => /卡密充值/.test(textOf(element))); const freeModeButton = modeButtons.find((element) => /免费充值/.test(textOf(element))); - const compactCardKey = String(rawCardKey || '').trim().replace(/\s+/g, ''); - const explicitSegments = compactCardKey.includes('-') || compactCardKey.includes('_') - ? compactCardKey.split(/[-_]+/).filter(Boolean) + const cardSegments = Array.isArray(rawCardSegments) + ? rawCardSegments.map((segment) => String(segment || '').replace(/[^A-Z0-9]/gi, '').toUpperCase()).slice(0, 3) : []; - 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); const isCardModeActive = Boolean(cardModeButton && hasActiveMarker(cardModeButton)) @@ -1564,10 +1575,58 @@ function FindProxyForURL(url, host) { input.className, ].join(' '))); if (fallbackInput) { - dispatchInput(fallbackInput, compactCardKey); + dispatchInput(fallbackInput, `GPC-${cardSegments.join('-')}`); } } + const startButton = Array.from(document.querySelectorAll('button, [role="button"]')) + .find((button) => /开始\s*Plus\s*充值|任务进行中/.test(textOf(button))); + const filledSegments = cardInputs.map((input) => String(input.value || '').replace(/\s+/g, '').toUpperCase()); + const cardKeyMatches = cardSegments.length === 3 + && cardInputs.length >= 3 + && cardSegments.every((segment, index) => filledSegments[index] === segment); + return { + ok: true, + cardInputCount: cardInputs.length, + cardFallbackFilled: Boolean(fallbackInput), + cardSegments: cardSegments.map((segment) => segment ? segment.length : 0), + cardKeyMatches, + cardKeyValue: filledSegments.join('-') || String(fallbackInput?.value || ''), + isCardModeActive, + startButtonText: textOf(startButton), + activeModeText: textOf(isCardModeActive ? cardModeButton : freeModeButton), + url: location.href, + }; + }, + args: [cardSegments], + }); + const result = results?.[0]?.result || {}; + if (!result?.ok) { + throw new Error('步骤 6:GPC 页面准备失败。'); + } + if (!result.isCardModeActive || (!result.cardInputCount && !result.cardFallbackFilled)) { + throw new Error('步骤 6:GPC 页面未进入卡密充值模式,无法填写卡密。'); + } + if (result.cardInputCount >= 3 && !result.cardKeyMatches) { + throw new Error(`步骤 6:GPC 卡密填写校验失败,当前页面为 ${result.cardKeyValue || '空'}。`); + } + return result; + } + + async function fillGpcSessionOnPortalPage(tabId, sessionJson = '') { + if (!chrome?.scripting?.executeScript) { + throw new Error('步骤 6:当前运行环境不支持脚本注入,无法填写 GPC session。'); + } + const results = await chrome.scripting.executeScript({ + target: { tabId }, + func: (rawSessionJson) => { + 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 sessionTextarea = Array.from(document.querySelectorAll('textarea')).find((textarea) => /session|accessToken|完整/i.test([ textarea.placeholder, textarea.name, @@ -1578,37 +1637,27 @@ function FindProxyForURL(url, host) { 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, - cardFallbackFilled: Boolean(fallbackInput), - cardSegments: cardSegments.map((segment) => segment ? segment.length : 0), - isCardModeActive, sessionLength: String(sessionTextarea.value || '').length, - startButtonText: textOf(startButton), - activeModeText: textOf(isCardModeActive ? cardModeButton : freeModeButton), - url: location.href, }; }, - args: [cardKey, sessionJson], + args: [sessionJson], }); const result = results?.[0]?.result || {}; if (!result?.ok) { - throw new Error('步骤 6:GPC 页面准备失败。'); - } - if (!result.isCardModeActive || (!result.cardInputCount && !result.cardFallbackFilled)) { - throw new Error('步骤 6:GPC 页面未进入卡密充值模式,无法填写卡密。'); + throw new Error('步骤 6:GPC session 填写失败。'); } return result; } async function executeGpcCheckoutCreate(state = {}) { const cardKey = resolveGpcCardKey(state); + const cardSegments = resolveGpcCardKeySegments(cardKey); await addLog('步骤 6:正在打开 GPC 页面并准备卡密充值模式...', 'info'); const { tabId, portalUrl } = await openOrReuseGpcPortalTab(state); + await addLog('步骤 6:正在切换 GPC 卡密充值模式并校验卡密...', 'info'); + const prepared = await prepareGpcCardKeyOnPortalPage(tabId, cardSegments); await addLog('步骤 6:正在从 ChatGPT 获取完整 session...', 'info'); const sessionTabId = await openFreshChatGptTabForCheckoutCreate({ active: false }); if (chrome?.tabs?.update) { @@ -1630,8 +1679,8 @@ function FindProxyForURL(url, host) { 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 addLog('步骤 6:GPC 卡密已确认,正在填写 ChatGPT session...', 'info'); + const sessionFilled = await fillGpcSessionOnPortalPage(tabId, sessionJson); await setState({ plusCheckoutTabId: tabId, plusCheckoutUrl: portalUrl, @@ -1642,7 +1691,7 @@ function FindProxyForURL(url, host) { gpcPageStatusText: '页面已准备', }); await addLog( - `步骤 6:GPC 页面已准备完成(卡密 ${prepared.cardInputCount || 0} 段,session ${prepared.sessionLength || 0} 字符),准备继续下一步。`, + `步骤 6:GPC 页面已准备完成(卡密 ${prepared.cardInputCount || 0} 段,session ${sessionFilled.sessionLength || 0} 字符),准备继续下一步。`, 'ok' ); await completeNodeFromBackground('plus-checkout-create', { diff --git a/tests/plus-checkout-create-wait.test.js b/tests/plus-checkout-create-wait.test.js index 8de28c5..aea006a 100644 --- a/tests/plus-checkout-create-wait.test.js +++ b/tests/plus-checkout-create-wait.test.js @@ -958,7 +958,12 @@ test('GPC checkout prepare opens page, fills segmented card key, and writes full }, scripting: { executeScript: async (details) => { - events.push({ type: 'execute-script', target: details.target, args: details.args }); + events.push({ + type: 'execute-script', + target: details.target, + args: details.args, + phase: Array.isArray(details.args?.[0]) ? 'card-key' : 'session', + }); const previous = { document: global.document, window: global.window, @@ -999,20 +1004,31 @@ test('GPC checkout prepare opens page, fills segmented card key, and writes full await executor.executePlusCheckoutCreate({ plusPaymentMethod: 'gpc-helper', - gpcCardKey: 'AAAA1111BBBB2222CCCC3333', + gpcCardKey: 'GPC-AAAA1111-BBBB2222-CCCC3333', }); assert.equal(dom.cardModeButton.clicked, true); assert.deepEqual(dom.cardInputs.map((input) => input.value), ['AAAA1111', 'BBBB2222', 'CCCC3333']); assert.equal(dom.sessionTextarea.value, JSON.stringify(session)); + const cardFillIndex = events.findIndex((event) => event.type === 'execute-script' && event.phase === 'card-key'); + const chatGptCreateIndex = events.findIndex((event) => ( + event.type === 'automation-tab-create' + && event.payload?.url === 'https://chatgpt.com/' + )); + const sessionFillIndex = events.findIndex((event) => event.type === 'execute-script' && event.phase === 'session' && Array.isArray(event.args)); + assert.notEqual(cardFillIndex, -1); + assert.notEqual(chatGptCreateIndex, -1); + assert.notEqual(sessionFillIndex, -1); + assert.equal(cardFillIndex < chatGptCreateIndex, true); + assert.equal(chatGptCreateIndex < sessionFillIndex, true); const sessionMessage = events.find((event) => event.type === 'tab-message'); assert.deepEqual(sessionMessage.message.payload, { includeSession: true, includeAccessToken: true, }); - const executeEvent = events.find((event) => event.type === 'execute-script' && Array.isArray(event.args)); + const executeEvent = events.find((event) => event.type === 'execute-script' && event.phase === 'session' && Array.isArray(event.args)); assert.equal(executeEvent.target.tabId, 77); - assert.equal(executeEvent.args[1], JSON.stringify(session)); + assert.equal(executeEvent.args[0], JSON.stringify(session)); const chatGptTabCreate = events.find((event) => ( event.type === 'automation-tab-create' && event.payload?.url === 'https://chatgpt.com/'