From a36fa708680340f8d13d5e46484905c0982ec26e Mon Sep 17 00:00:00 2001 From: QLHazyCoder <2825305047@qq.com> Date: Fri, 29 May 2026 14:37:34 +0800 Subject: [PATCH] fix: keep GPC card mode during plus billing --- .../background/steps/create-plus-checkout.js | 95 +++++++++++-- .../background/steps/fill-plus-checkout.js | 129 +++++++++++++++++- ...us-checkout-billing-tab-resolution.test.js | 37 +++++ tests/plus-checkout-create-wait.test.js | 8 +- 4 files changed, 253 insertions(+), 16 deletions(-) diff --git a/flows/openai/background/steps/create-plus-checkout.js b/flows/openai/background/steps/create-plus-checkout.js index a0e6417..30fb819 100644 --- a/flows/openai/background/steps/create-plus-checkout.js +++ b/flows/openai/background/steps/create-plus-checkout.js @@ -1396,6 +1396,69 @@ function FindProxyForURL(url, host) { } await waitForTabCompleteUntilStopped(tabId); await sleepWithStop(800); + + const ensureCardMode = async () => { + const results = await chrome.scripting.executeScript({ + target: { tabId }, + func: () => { + 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 hasActiveMarker = (element) => { + if (!element) return false; + const markerText = [ + element.className, + element.getAttribute?.('class'), + element.getAttribute?.('aria-selected'), + element.getAttribute?.('aria-pressed'), + element.getAttribute?.('data-state'), + element.getAttribute?.('data-active'), + ].join(' '); + return /\b(active|selected|current|checked|on|true)\b/i.test(markerText); + }; + const candidates = Array.from(document.querySelectorAll('button, [role="button"], .design-mode-card, .mode-card, [class*="mode"]')); + const cardModeButton = candidates.find((element) => /卡密充值/.test(textOf(element))); + const freeModeButton = candidates.find((element) => /免费充值/.test(textOf(element))); + const visibleCardInputs = Array.from(document.querySelectorAll('input.card-key-seg, input[placeholder*="XXXXXXXX"], input[maxlength="8"]')) + .filter(isVisible); + const isCardModeActive = Boolean(cardModeButton && hasActiveMarker(cardModeButton)) + || (visibleCardInputs.length > 0 && !(freeModeButton && hasActiveMarker(freeModeButton))); + if (cardModeButton && !isCardModeActive) { + cardModeButton.scrollIntoView?.({ block: 'center', inline: 'center' }); + cardModeButton.click?.(); + return { + hasCardMode: true, + clickedCardMode: true, + isCardModeActive: false, + activeModeText: textOf(cardModeButton), + }; + } + return { + hasCardMode: Boolean(cardModeButton), + clickedCardMode: false, + isCardModeActive, + activeModeText: textOf(isCardModeActive ? cardModeButton : freeModeButton), + }; + }, + }); + return results?.[0]?.result || {}; + }; + + const modeResult = await ensureCardMode(); + if (!modeResult.hasCardMode) { + throw new Error('步骤 6:未找到 GPC“卡密充值”模式入口。'); + } + if (modeResult.clickedCardMode) { + await sleepWithStop(600); + } + const results = await chrome.scripting.executeScript({ target: { tabId }, func: (rawCardKey, rawSessionJson) => { @@ -1416,17 +1479,21 @@ function FindProxyForURL(url, host) { element.dispatchEvent?.(new Event('change', { bubbles: true })); element.blur?.(); }; - const clickElement = (element) => { - element.scrollIntoView?.({ block: 'center', inline: 'center' }); - element.click?.(); + const hasActiveMarker = (element) => { + if (!element) return false; + const markerText = [ + element.className, + element.getAttribute?.('class'), + element.getAttribute?.('aria-selected'), + element.getAttribute?.('aria-pressed'), + element.getAttribute?.('data-state'), + element.getAttribute?.('data-active'), + ].join(' '); + return /\b(active|selected|current|checked|on|true)\b/i.test(markerText); }; 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); - } + const freeModeButton = modeButtons.find((element) => /免费充值/.test(textOf(element))); const compactCardKey = String(rawCardKey || '').trim().replace(/\s+/g, ''); const explicitSegments = compactCardKey.includes('-') || compactCardKey.includes('_') @@ -1437,11 +1504,14 @@ function FindProxyForURL(url, host) { : (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)) + || (cardInputs.length > 0 && !(freeModeButton && hasActiveMarker(freeModeButton))); cardInputs.forEach((input, index) => { dispatchInput(input, cardSegments[index] || ''); }); + let fallbackInput = null; if (!cardInputs.length) { - const fallbackInput = Array.from(document.querySelectorAll('input')).find((input) => /卡密|card/i.test([ + fallbackInput = Array.from(document.querySelectorAll('input')).find((input) => /卡密|card/i.test([ input.placeholder, input.name, input.id, @@ -1468,10 +1538,12 @@ function FindProxyForURL(url, host) { 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(cardModeButton), + activeModeText: textOf(isCardModeActive ? cardModeButton : freeModeButton), url: location.href, }; }, @@ -1481,6 +1553,9 @@ function FindProxyForURL(url, host) { if (!result?.ok) { throw new Error('步骤 6:GPC 页面准备失败。'); } + if (!result.isCardModeActive || (!result.cardInputCount && !result.cardFallbackFilled)) { + throw new Error('步骤 6:GPC 页面未进入卡密充值模式,无法填写卡密。'); + } return result; } diff --git a/flows/openai/background/steps/fill-plus-checkout.js b/flows/openai/background/steps/fill-plus-checkout.js index dae8f29..1a9bf85 100644 --- a/flows/openai/background/steps/fill-plus-checkout.js +++ b/flows/openai/background/steps/fill-plus-checkout.js @@ -197,14 +197,41 @@ target: { tabId }, func: () => { 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 hasActiveMarker = (element) => { + if (!element) return false; + const markerText = [ + element.className, + element.getAttribute?.('class'), + element.getAttribute?.('aria-selected'), + element.getAttribute?.('aria-pressed'), + element.getAttribute?.('data-state'), + element.getAttribute?.('data-active'), + ].join(' '); + return /\b(active|selected|current|checked|on|true)\b/i.test(markerText); + }; 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 modeButtons = Array.from(document.querySelectorAll('button, [role="button"], .design-mode-card, .mode-card, [class*="mode"]')); + const cardModeButton = modeButtons.find((element) => /卡密充值/.test(textOf(element))); + const freeModeButton = modeButtons.find((element) => /免费充值/.test(textOf(element))); 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 cardInputs = Array.from(document.querySelectorAll('input.card-key-seg, input[placeholder*="XXXXXXXX"], input[maxlength="8"]')) + .filter(isVisible); const sessionTextarea = document.querySelector('textarea.design-session-input') || document.querySelector('textarea'); + const isCardModeActive = Boolean(cardModeButton && hasActiveMarker(cardModeButton)) + || (cardInputs.length > 0 && !(freeModeButton && hasActiveMarker(freeModeButton))); return { url: location.href, readyState: document.readyState, @@ -217,6 +244,74 @@ hasStartButton: Boolean(startButton), cardKeyValue: cardInputs.map((input) => String(input.value || '')).join('-'), sessionLength: String(sessionTextarea?.value || '').length, + hasCardMode: Boolean(cardModeButton), + hasFreeMode: Boolean(freeModeButton), + isCardModeActive, + activeModeText: textOf(isCardModeActive ? cardModeButton : freeModeButton), + }; + }, + }); + return results?.[0]?.result || {}; + } + + async function ensureGpcCardMode(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 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 hasActiveMarker = (element) => { + if (!element) return false; + const markerText = [ + element.className, + element.getAttribute?.('class'), + element.getAttribute?.('aria-selected'), + element.getAttribute?.('aria-pressed'), + element.getAttribute?.('data-state'), + element.getAttribute?.('data-active'), + ].join(' '); + return /\b(active|selected|current|checked|on|true)\b/i.test(markerText); + }; + const candidates = Array.from(document.querySelectorAll('button, [role="button"], .design-mode-card, .mode-card, [class*="mode"]')); + const cardModeButton = candidates.find((element) => /卡密充值/.test(textOf(element))); + const freeModeButton = candidates.find((element) => /免费充值/.test(textOf(element))); + const cardInputs = Array.from(document.querySelectorAll('input.card-key-seg, input[placeholder*="XXXXXXXX"], input[maxlength="8"]')) + .filter(isVisible); + const isCardModeActive = Boolean(cardModeButton && hasActiveMarker(cardModeButton)) + || (cardInputs.length > 0 && !(freeModeButton && hasActiveMarker(freeModeButton))); + if (!cardModeButton) { + return { + ok: false, + reason: 'missing-card-mode', + activeModeText: textOf(freeModeButton), + }; + } + if (isCardModeActive) { + return { + ok: true, + clicked: false, + isCardModeActive: true, + activeModeText: textOf(cardModeButton), + }; + } + cardModeButton.scrollIntoView?.({ block: 'center', inline: 'center' }); + cardModeButton.click?.(); + return { + ok: true, + clicked: true, + isCardModeActive: false, + activeModeText: textOf(cardModeButton), }; }, }); @@ -231,7 +326,16 @@ 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 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 buttons = Array.from(document.querySelectorAll('button, [role="button"]')).filter(isVisible); const button = buttons.find((candidate) => /开始\s*Plus\s*充值/.test(textOf(candidate))); if (!button) { return { @@ -261,9 +365,7 @@ async function executeGpcHelperBilling(state = {}) { 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(() => {}) - )); + await chrome.tabs.update(tabId, { active: true }).catch(() => {}); } await addLog('步骤 7:正在等待 GPC 页面加载完成...', 'info'); await waitForTabCompleteUntilStopped(tabId); @@ -311,6 +413,23 @@ continue; } + if (!pageState.isCardModeActive) { + const modeResult = await ensureGpcCardMode(tabId); + if (!modeResult.ok) { + throw new Error('GPC_PAGE_FLOW_ENDED::步骤 7:未找到 GPC“卡密充值”模式入口,无法启动 Plus 充值。'); + } + await addLog( + modeResult.clicked + ? '步骤 7:已切换到 GPC 卡密充值模式,准备启动。' + : '步骤 7:GPC 卡密充值模式已就绪。', + 'info' + ); + if (modeResult.clicked) { + await sleepWithStop(800); + } + 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} 次仍未显示订阅完成。`); diff --git a/tests/plus-checkout-billing-tab-resolution.test.js b/tests/plus-checkout-billing-tab-resolution.test.js index f4b7064..e119dad 100644 --- a/tests/plus-checkout-billing-tab-resolution.test.js +++ b/tests/plus-checkout-billing-tab-resolution.test.js @@ -903,10 +903,23 @@ test('Plus checkout billing reports when the payment iframe exists but cannot re function createGpcPageHarness(states) { const pageStates = Array.isArray(states) ? [...states] : []; const clicks = []; + const modeClicks = []; return { clicks, + modeClicks, run(details) { const source = String(details.func || ''); + if (source.includes('cardModeButton.click')) { + modeClicks.push({ target: details.target?.tabId }); + return [{ + result: { + ok: true, + clicked: true, + isCardModeActive: false, + activeModeText: '卡密充值 使用付费卡密扣次充值', + }, + }]; + } if (source.includes('button.click')) { clicks.push({ target: details.target?.tabId }); return [{ result: { clicked: true, buttonText: '开始 Plus 充值' } }]; @@ -925,6 +938,10 @@ function createGpcPageHarness(states) { hasStartButton: true, cardKeyValue: 'AAAA1111-BBBB2222-CCCC3333', sessionLength: 256, + hasCardMode: state.hasCardMode !== false, + hasFreeMode: true, + isCardModeActive: state.isCardModeActive !== false, + activeModeText: state.isCardModeActive === false ? '免费充值' : '卡密充值 使用付费卡密扣次充值', }, }]; }, @@ -1005,6 +1022,26 @@ test('GPC billing clicks start and completes after page log shows subscription d assert.equal(events.states.some((state) => state.gpcPageStatus === 'completed'), true); }); +test('GPC billing keeps prepared page, switches card mode, and clicks start instead of reloading home', async () => { + const { events, executor, pageHarness } = createGpcPageExecutorHarness([ + { startButtonText: '停止当前任务', logText: '免费充值任务进行中', isCardModeActive: false }, + { startButtonText: '开始 Plus 充值', logText: 'SYSTEM 页面已就绪', isCardModeActive: true }, + { startButtonText: '任务进行中', logText: '开始处理任务', isCardModeActive: true }, + { startButtonText: '开始 Plus 充值', logText: '订阅完成', hasSubscriptionDone: true, isCardModeActive: true }, + ]); + + await executor.executePlusCheckoutBilling({ + plusPaymentMethod: 'gpc-helper', + plusCheckoutSource: 'gpc-helper', + plusCheckoutTabId: 77, + }); + + assert.equal(pageHarness.modeClicks.length, 1); + assert.equal(pageHarness.clicks.length, 1); + assert.equal(events.updates.some((event) => event.payload?.url === 'https://gpc.qlhazycoder.top/'), false); + assert.equal(events.completed.length, 1); +}); + test('GPC billing restarts when start button returns without subscription done', async () => { const { events, executor, pageHarness } = createGpcPageExecutorHarness([ { startButtonText: '开始 Plus 充值', logText: 'SYSTEM 页面已就绪' }, diff --git a/tests/plus-checkout-create-wait.test.js b/tests/plus-checkout-create-wait.test.js index ed6682a..a396ccd 100644 --- a/tests/plus-checkout-create-wait.test.js +++ b/tests/plus-checkout-create-wait.test.js @@ -873,6 +873,7 @@ function createFakeGpcPrepareDom() { scrollIntoView() {}, click() { this.clicked = true; + this.onClick?.(); }, dispatchEvent(event) { dispatches.push({ element: this, type: event?.type || '' }); @@ -886,6 +887,11 @@ function createFakeGpcPrepareDom() { const freeButton = createElement({ tagName: 'BUTTON', text: '免费充值' }); const cardModeButton = createElement({ tagName: 'BUTTON', text: '卡密充值 使用付费卡密扣次充值' }); + freeButton.className = 'active'; + cardModeButton.onClick = () => { + freeButton.className = ''; + cardModeButton.className = 'active'; + }; const startButton = createElement({ tagName: 'BUTTON', text: '开始 Plus 充值' }); const cardInputs = [ createElement({ tagName: 'INPUT', placeholder: 'XXXXXXXX', className: 'card-key-seg' }), @@ -1004,7 +1010,7 @@ test('GPC checkout prepare opens page, fills segmented card key, and writes full includeSession: true, includeAccessToken: true, }); - const executeEvent = events.find((event) => event.type === 'execute-script'); + const executeEvent = events.find((event) => event.type === 'execute-script' && Array.isArray(event.args)); assert.equal(executeEvent.target.tabId, 77); assert.equal(executeEvent.args[1], JSON.stringify(session)); const statePayload = events.find((event) => event.type === 'set-state')?.payload || {};