diff --git a/background/steps/create-plus-checkout.js b/background/steps/create-plus-checkout.js index 6e5d150..905e874 100644 --- a/background/steps/create-plus-checkout.js +++ b/background/steps/create-plus-checkout.js @@ -210,13 +210,42 @@ return Number.isFinite(numeric) ? Math.max(0, Math.floor(numeric)) : null; } - function isGpcAutoModeEnabled(payload = {}) { - const rootScope = typeof self !== 'undefined' ? self : globalThis; - if (rootScope.GoPayUtils?.isGpcAutoModeEnabled) { - return rootScope.GoPayUtils.isGpcAutoModeEnabled(payload); + 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); - return data?.auto_mode_enabled === true || data?.autoModeEnabled === true; + 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 = '') { @@ -244,7 +273,7 @@ if (remainingUses !== null && remainingUses <= 0) { throw new Error('创建 GPC 订单失败:API Key 剩余次数不足。'); } - if (phoneMode === GPC_HELPER_PHONE_MODE_AUTO && !isGpcAutoModeEnabled(balanceData)) { + if (phoneMode === GPC_HELPER_PHONE_MODE_AUTO && isGpcAutoModePermissionDenied(balanceData)) { throw new Error('创建 GPC 订单失败:当前 GPC API Key 未开通自动模式。'); } } diff --git a/sidepanel/sidepanel.js b/sidepanel/sidepanel.js index 2bbb36d..d32e7e0 100644 --- a/sidepanel/sidepanel.js +++ b/sidepanel/sidepanel.js @@ -7775,7 +7775,7 @@ async function ensureGpcApiKeyReadyForStart(options = {}) { return false; } - if (selectedMode === GPC_HELPER_PHONE_MODE_AUTO && !balanceState.gopayHelperAutoModeEnabled) { + if (selectedMode === GPC_HELPER_PHONE_MODE_AUTO && isGpcAutoModePermissionDenied(balanceState)) { if (typeof selectGpcHelperPhoneMode !== 'undefined' && selectGpcHelperPhoneMode) { selectGpcHelperPhoneMode.value = GPC_HELPER_PHONE_MODE_MANUAL; } @@ -11805,15 +11805,22 @@ btnGpcHelperBalance?.addEventListener('click', async () => { gopayHelperAutoModeEnabled: getGpcAutoModeEnabledFromResponse(response), gopayHelperApiKeyStatus: response?.apiKeyStatus || response?.data?.status || response?.payload?.data?.status || response?.payload?.status || '', }; + const nextAutoModePermission = getGpcAutoModePermissionFromPayload(nextState.gopayHelperBalancePayload); + const nextAutoModeDenied = nextAutoModePermission === false; + const nextAutoModeConfirmed = nextAutoModePermission === true || nextState.gopayHelperAutoModeEnabled; const selectedModeBeforeBalanceState = getSelectedGpcHelperPhoneMode(); syncLatestState(nextState); - if (!nextState.gopayHelperAutoModeEnabled && selectedModeBeforeBalanceState === GPC_HELPER_PHONE_MODE_AUTO) { + if (nextAutoModeDenied && selectedModeBeforeBalanceState === GPC_HELPER_PHONE_MODE_AUTO) { selectGpcHelperPhoneMode.value = GPC_HELPER_PHONE_MODE_MANUAL; syncLatestState({ gopayHelperPhoneMode: GPC_HELPER_PHONE_MODE_MANUAL }); await saveSettings({ silent: true, force: true }).catch(() => {}); showToast('当前 API Key 未开通自动模式,已切回手动模式。', 'warn'); + } else if (nextAutoModeDenied) { + showToast('GPC 余额已更新,当前 API Key 只能使用手动模式。', 'success'); + } else if (nextAutoModeConfirmed) { + showToast('GPC 余额已更新,自动模式可用。', 'success'); } else { - showToast(nextState.gopayHelperAutoModeEnabled ? 'GPC 余额已更新,自动模式可用。' : 'GPC 余额已更新,当前 API Key 只能使用手动模式。', 'success'); + showToast('GPC 余额已更新,当前接口未返回自动模式权限,已保留所选模式。', 'success'); } updatePlusModeUI(); } catch (error) { diff --git a/tests/plus-checkout-create-wait.test.js b/tests/plus-checkout-create-wait.test.js index 9b3d01a..6c13520 100644 --- a/tests/plus-checkout-create-wait.test.js +++ b/tests/plus-checkout-create-wait.test.js @@ -510,6 +510,63 @@ test('GPC auto checkout only sends access token and API Key', async () => { assert.equal(events.find((event) => event.type === 'complete')?.step, 6); }); +test('GPC auto checkout keeps running when balance payload omits auto mode permission', async () => { + const events = []; + const fetchCalls = []; + const executor = api.createPlusCheckoutCreateExecutor({ + addLog: async () => {}, + chrome: { + tabs: { + create: async () => { + throw new Error('should not open token tab when direct access token exists'); + }, + remove: async () => {}, + }, + }, + completeStepFromBackground: async (step, payload) => events.push({ type: 'complete', step, payload }), + ensureContentScriptReadyOnTabUntilStopped: async () => {}, + fetch: async (url, options = {}) => { + fetchCalls.push({ url, options }); + return { + ok: true, + status: 200, + json: async () => url.endsWith('/api/gp/balance') + ? createGpcBalanceResponse({ auto_mode_enabled: undefined, remaining_uses: 998 }) + : createGpcTaskResponse({ + task_id: 'task_auto_unknown_permission', + status: 'queued', + status_text: '排队中', + phone_mode: 'auto', + api_waiting_for: '', + }), + }; + }, + registerTab: async () => {}, + sendTabMessageUntilStopped: async () => ({}), + setState: async (payload) => events.push({ type: 'set-state', payload }), + sleepWithStop: async () => {}, + waitForTabCompleteUntilStopped: async () => {}, + }); + + await executor.executePlusCheckoutCreate({ + plusPaymentMethod: 'gpc-helper', + gopayHelperPhoneMode: 'auto', + gopayHelperApiUrl: 'https://gpc.qlhazycoder.top/', + chatgptAccessToken: 'state-access-token', + gopayHelperApiKey: 'gpc_auto_123', + }); + + assert.equal(fetchCalls.length, 2); + assert.equal(fetchCalls[0].url, 'https://gpc.qlhazycoder.top/api/gp/balance'); + assert.equal(fetchCalls[1].url, 'https://gpc.qlhazycoder.top/api/gp/tasks'); + const helperPayload = JSON.parse(fetchCalls[1].options.body); + assert.equal(helperPayload.phone_mode, 'auto'); + const statePayload = events.find((event) => event.type === 'set-state')?.payload || {}; + assert.equal(statePayload.gopayHelperTaskId, 'task_auto_unknown_permission'); + assert.equal(statePayload.gopayHelperPhoneMode, 'auto'); + assert.equal(events.find((event) => event.type === 'complete')?.step, 6); +}); + test('GPC auto checkout blocks API Keys without auto mode permission', async () => { const fetchCalls = []; const executor = api.createPlusCheckoutCreateExecutor({ diff --git a/tests/sidepanel-plus-payment-method.test.js b/tests/sidepanel-plus-payment-method.test.js index 92c0e6b..fdd8e6d 100644 --- a/tests/sidepanel-plus-payment-method.test.js +++ b/tests/sidepanel-plus-payment-method.test.js @@ -502,6 +502,70 @@ return { updatePlusModeUI, selectGpcHelperPhoneMode, plusPaymentMethodCaption, r assert.match(api.plusPaymentMethodCaption.textContent, /自动/); }); +test('sidepanel start check keeps GPC auto mode when balance payload omits permission field', async () => { + const bundle = [ + extractFunction('normalizeGpcAutoModePermissionValue'), + extractFunction('getGpcAutoModePermissionFromPayload'), + extractFunction('isGpcAutoModePermissionDenied'), + extractFunction('normalizeGpcRemainingUsesValue'), + extractFunction('ensureGpcApiKeyReadyForStart'), + ].join('\n'); + + const api = new Function(` +let latestState = { gopayHelperPhoneMode: 'auto' }; +const GPC_HELPER_PHONE_MODE_AUTO = 'auto'; +const GPC_HELPER_PHONE_MODE_MANUAL = 'manual'; +const selectGpcHelperPhoneMode = { value: 'auto' }; +const dialogs = []; +let saveCalls = 0; +let updateCalls = 0; +${bundle} +function isGpcHelperCheckoutSelected() { return true; } +function getSelectedGpcHelperPhoneMode() { return selectGpcHelperPhoneMode.value; } +async function refreshGpcBalanceForStart() { + return { + gopayHelperRemainingUses: 998, + gopayHelperApiKeyStatus: 'active', + gopayHelperAutoModeEnabled: false, + gopayHelperBalancePayload: { + status: 'active', + remaining_uses: 998, + }, + }; +} +async function showGpcStartBlockedDialog(message) { + dialogs.push(message); +} +function syncLatestState(nextState) { + latestState = { ...latestState, ...nextState }; +} +function updatePlusModeUI() { + updateCalls += 1; +} +async function saveSettings() { + saveCalls += 1; +} +function showToast() {} +return { + ensureGpcApiKeyReadyForStart, + selectGpcHelperPhoneMode, + getDialogs: () => dialogs.slice(), + getSaveCalls: () => saveCalls, + getUpdateCalls: () => updateCalls, + getPersistedPhoneMode: () => latestState.gopayHelperPhoneMode, +}; +`)(); + + const allowed = await api.ensureGpcApiKeyReadyForStart(); + + assert.equal(allowed, true); + assert.equal(api.selectGpcHelperPhoneMode.value, 'auto'); + assert.equal(api.getPersistedPhoneMode(), 'auto'); + assert.equal(api.getSaveCalls(), 0); + assert.equal(api.getUpdateCalls(), 0); + assert.deepEqual(api.getDialogs(), []); +}); + test('sidepanel resolves pending GoPay manual confirmation from DATA_UPDATED state', async () => { const bundle = [ extractFunction('openPlusManualConfirmationDialog'),