fix: validate GPC card key before session fill

This commit is contained in:
QLHazyCoder
2026-05-29 14:58:32 +08:00
parent 02706da94a
commit 27cbf2a61b
2 changed files with 96 additions and 31 deletions
@@ -1282,6 +1282,21 @@ function FindProxyForURL(url, host) {
return cardKey; 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 = {}) { function buildGpcPortalUrl(state = {}) {
const baseUrl = normalizeGpcBaseUrl(state?.gpcBaseUrl || DEFAULT_GPC_BASE_URL) const baseUrl = normalizeGpcBaseUrl(state?.gpcBaseUrl || DEFAULT_GPC_BASE_URL)
.replace(/\/+$/g, ''); .replace(/\/+$/g, '');
@@ -1391,7 +1406,7 @@ function FindProxyForURL(url, host) {
return session; return session;
} }
async function prepareGpcPortalPage(tabId, cardKey = '', sessionJson = '') { async function prepareGpcCardKeyOnPortalPage(tabId, cardSegments = []) {
if (!chrome?.scripting?.executeScript) { if (!chrome?.scripting?.executeScript) {
throw new Error('步骤 6:当前运行环境不支持脚本注入,无法填写 GPC 页面。'); throw new Error('步骤 6:当前运行环境不支持脚本注入,无法填写 GPC 页面。');
} }
@@ -1507,7 +1522,7 @@ function FindProxyForURL(url, host) {
const results = await chrome.scripting.executeScript({ const results = await chrome.scripting.executeScript({
target: { tabId }, target: { tabId },
func: (rawCardKey, rawSessionJson) => { func: (rawCardSegments) => {
const textOf = (element) => String(element?.innerText || element?.textContent || element?.value || '').replace(/\s+/g, ' ').trim(); const textOf = (element) => String(element?.innerText || element?.textContent || element?.value || '').replace(/\s+/g, ' ').trim();
const isVisible = (element) => { const isVisible = (element) => {
if (!element) return false; if (!element) return false;
@@ -1541,13 +1556,9 @@ function FindProxyForURL(url, host) {
const cardModeButton = modeButtons.find((element) => /卡密充值/.test(textOf(element))); const cardModeButton = modeButtons.find((element) => /卡密充值/.test(textOf(element)));
const freeModeButton = modeButtons.find((element) => /免费充值/.test(textOf(element))); const freeModeButton = modeButtons.find((element) => /免费充值/.test(textOf(element)));
const compactCardKey = String(rawCardKey || '').trim().replace(/\s+/g, ''); const cardSegments = Array.isArray(rawCardSegments)
const explicitSegments = compactCardKey.includes('-') || compactCardKey.includes('_') ? rawCardSegments.map((segment) => String(segment || '').replace(/[^A-Z0-9]/gi, '').toUpperCase()).slice(0, 3)
? 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"]')) const cardInputs = Array.from(document.querySelectorAll('input.card-key-seg, input[placeholder*="XXXXXXXX"], input[maxlength="8"]'))
.filter(isVisible); .filter(isVisible);
const isCardModeActive = Boolean(cardModeButton && hasActiveMarker(cardModeButton)) const isCardModeActive = Boolean(cardModeButton && hasActiveMarker(cardModeButton))
@@ -1564,10 +1575,58 @@ function FindProxyForURL(url, host) {
input.className, input.className,
].join(' '))); ].join(' ')));
if (fallbackInput) { 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('步骤 6GPC 页面准备失败。');
}
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([ const sessionTextarea = Array.from(document.querySelectorAll('textarea')).find((textarea) => /session|accessToken|完整/i.test([
textarea.placeholder, textarea.placeholder,
textarea.name, textarea.name,
@@ -1578,37 +1637,27 @@ function FindProxyForURL(url, host) {
throw new Error('未找到 GPC session 输入框。'); throw new Error('未找到 GPC session 输入框。');
} }
dispatchInput(sessionTextarea, String(rawSessionJson || '')); dispatchInput(sessionTextarea, String(rawSessionJson || ''));
const startButton = Array.from(document.querySelectorAll('button, [role="button"]'))
.find((button) => /开始\s*Plus\s*充值|任务进行中/.test(textOf(button)));
return { return {
ok: true, ok: true,
cardInputCount: cardInputs.length,
cardFallbackFilled: Boolean(fallbackInput),
cardSegments: cardSegments.map((segment) => segment ? segment.length : 0),
isCardModeActive,
sessionLength: String(sessionTextarea.value || '').length, 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 || {}; const result = results?.[0]?.result || {};
if (!result?.ok) { if (!result?.ok) {
throw new Error('步骤 6GPC 页面准备失败。'); throw new Error('步骤 6GPC session 填写失败。');
}
if (!result.isCardModeActive || (!result.cardInputCount && !result.cardFallbackFilled)) {
throw new Error('步骤 6:GPC 页面未进入卡密充值模式,无法填写卡密。');
} }
return result; return result;
} }
async function executeGpcCheckoutCreate(state = {}) { async function executeGpcCheckoutCreate(state = {}) {
const cardKey = resolveGpcCardKey(state); const cardKey = resolveGpcCardKey(state);
const cardSegments = resolveGpcCardKeySegments(cardKey);
await addLog('步骤 6:正在打开 GPC 页面并准备卡密充值模式...', 'info'); await addLog('步骤 6:正在打开 GPC 页面并准备卡密充值模式...', 'info');
const { tabId, portalUrl } = await openOrReuseGpcPortalTab(state); const { tabId, portalUrl } = await openOrReuseGpcPortalTab(state);
await addLog('步骤 6:正在切换 GPC 卡密充值模式并校验卡密...', 'info');
const prepared = await prepareGpcCardKeyOnPortalPage(tabId, cardSegments);
await addLog('步骤 6:正在从 ChatGPT 获取完整 session...', 'info'); await addLog('步骤 6:正在从 ChatGPT 获取完整 session...', 'info');
const sessionTabId = await openFreshChatGptTabForCheckoutCreate({ active: false }); const sessionTabId = await openFreshChatGptTabForCheckoutCreate({ active: false });
if (chrome?.tabs?.update) { if (chrome?.tabs?.update) {
@@ -1630,8 +1679,8 @@ function FindProxyForURL(url, host) {
if (chrome?.tabs?.update) { if (chrome?.tabs?.update) {
await chrome.tabs.update(tabId, { active: true }).catch(() => {}); await chrome.tabs.update(tabId, { active: true }).catch(() => {});
} }
await addLog('步骤 6正在填写 GPC 卡密 ChatGPT session...', 'info'); await addLog('步骤 6GPC 卡密已确认,正在填写 ChatGPT session...', 'info');
const prepared = await prepareGpcPortalPage(tabId, cardKey, sessionJson); const sessionFilled = await fillGpcSessionOnPortalPage(tabId, sessionJson);
await setState({ await setState({
plusCheckoutTabId: tabId, plusCheckoutTabId: tabId,
plusCheckoutUrl: portalUrl, plusCheckoutUrl: portalUrl,
@@ -1642,7 +1691,7 @@ function FindProxyForURL(url, host) {
gpcPageStatusText: '页面已准备', gpcPageStatusText: '页面已准备',
}); });
await addLog( await addLog(
`步骤 6:GPC 页面已准备完成(卡密 ${prepared.cardInputCount || 0} 段,session ${prepared.sessionLength || 0} 字符),准备继续下一步。`, `步骤 6:GPC 页面已准备完成(卡密 ${prepared.cardInputCount || 0} 段,session ${sessionFilled.sessionLength || 0} 字符),准备继续下一步。`,
'ok' 'ok'
); );
await completeNodeFromBackground('plus-checkout-create', { await completeNodeFromBackground('plus-checkout-create', {
+20 -4
View File
@@ -958,7 +958,12 @@ test('GPC checkout prepare opens page, fills segmented card key, and writes full
}, },
scripting: { scripting: {
executeScript: async (details) => { 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 = { const previous = {
document: global.document, document: global.document,
window: global.window, window: global.window,
@@ -999,20 +1004,31 @@ test('GPC checkout prepare opens page, fills segmented card key, and writes full
await executor.executePlusCheckoutCreate({ await executor.executePlusCheckoutCreate({
plusPaymentMethod: 'gpc-helper', plusPaymentMethod: 'gpc-helper',
gpcCardKey: 'AAAA1111BBBB2222CCCC3333', gpcCardKey: 'GPC-AAAA1111-BBBB2222-CCCC3333',
}); });
assert.equal(dom.cardModeButton.clicked, true); assert.equal(dom.cardModeButton.clicked, true);
assert.deepEqual(dom.cardInputs.map((input) => input.value), ['AAAA1111', 'BBBB2222', 'CCCC3333']); assert.deepEqual(dom.cardInputs.map((input) => input.value), ['AAAA1111', 'BBBB2222', 'CCCC3333']);
assert.equal(dom.sessionTextarea.value, JSON.stringify(session)); 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'); const sessionMessage = events.find((event) => event.type === 'tab-message');
assert.deepEqual(sessionMessage.message.payload, { assert.deepEqual(sessionMessage.message.payload, {
includeSession: true, includeSession: true,
includeAccessToken: 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.target.tabId, 77);
assert.equal(executeEvent.args[1], JSON.stringify(session)); assert.equal(executeEvent.args[0], JSON.stringify(session));
const chatGptTabCreate = events.find((event) => ( const chatGptTabCreate = events.find((event) => (
event.type === 'automation-tab-create' event.type === 'automation-tab-create'
&& event.payload?.url === 'https://chatgpt.com/' && event.payload?.url === 'https://chatgpt.com/'