From 561c563bd720f218a8294788711ff648b5ffd679 Mon Sep 17 00:00:00 2001 From: cnJasonZ Date: Wed, 2 Jul 2025 15:49:37 +0800 Subject: [PATCH] PPIO OAuth Login (#7717) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: integrate PPIO OAuth login support Add OAuth authentication support for PPIO provider with complete integration: - Add PPIO OAuth configuration and client ID - Implement oauthWithPPIO authentication flow - Add PPIO to OAuth and charge-supported providers list - Include PPIO logo and UI components for OAuth settings - Support charge and billing URL redirects for PPIO 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude * fix: fix url * fix: fix redirect url * feat: add PPIO OAuth login * fix: migrate * fix: migrate * fix: ppio migrate --------- Co-authored-by: Claude --- .../src/components/OAuth/OAuthButton.tsx | 6 +- src/renderer/src/config/constant.ts | 2 + src/renderer/src/config/models.ts | 12 --- src/renderer/src/config/providers.ts | 5 +- .../ProviderSettings/ProviderOAuth.tsx | 7 +- src/renderer/src/services/ProviderService.ts | 4 +- src/renderer/src/store/index.ts | 2 +- src/renderer/src/store/migrate.ts | 28 ++++-- src/renderer/src/utils/oauth.ts | 87 ++++++++++++++++++- 9 files changed, 128 insertions(+), 25 deletions(-) diff --git a/src/renderer/src/components/OAuth/OAuthButton.tsx b/src/renderer/src/components/OAuth/OAuthButton.tsx index 37909041bf..aafbd81c5a 100644 --- a/src/renderer/src/components/OAuth/OAuthButton.tsx +++ b/src/renderer/src/components/OAuth/OAuthButton.tsx @@ -1,5 +1,5 @@ import { Provider } from '@renderer/types' -import { oauthWithAihubmix, oauthWithSiliconFlow, oauthWithTokenFlux } from '@renderer/utils/oauth' +import { oauthWithAihubmix, oauthWithPPIO, oauthWithSiliconFlow, oauthWithTokenFlux } from '@renderer/utils/oauth' import { Button, ButtonProps } from 'antd' import { FC } from 'react' import { useTranslation } from 'react-i18next' @@ -28,6 +28,10 @@ const OAuthButton: FC = ({ provider, onSuccess, ...buttonProps }) => { oauthWithAihubmix(handleSuccess) } + if (provider.id === 'ppio') { + oauthWithPPIO(handleSuccess) + } + if (provider.id === 'tokenflux') { oauthWithTokenFlux() } diff --git a/src/renderer/src/config/constant.ts b/src/renderer/src/config/constant.ts index 1a8a0d0fee..80e2be8a34 100644 --- a/src/renderer/src/config/constant.ts +++ b/src/renderer/src/config/constant.ts @@ -11,6 +11,8 @@ export const isWin = platform === 'win32' || platform === 'win64' export const isLinux = platform === 'linux' export const SILICON_CLIENT_ID = 'SFaJLLq0y6CAMoyDm81aMu' +export const PPIO_CLIENT_ID = '37d0828c96b34936a600b62c' +export const PPIO_APP_SECRET = import.meta.env.RENDERER_VITE_PPIO_APP_SECRET || '' export const TOKENFLUX_HOST = 'https://tokenflux.ai' // Messages loading configuration diff --git a/src/renderer/src/config/models.ts b/src/renderer/src/config/models.ts index 754162e6a7..9611aeb59f 100644 --- a/src/renderer/src/config/models.ts +++ b/src/renderer/src/config/models.ts @@ -844,18 +844,6 @@ export const SYSTEM_MODELS: Record = { provider: 'ppio', name: 'Qwen3 Reranker 8B', group: 'qwen' - }, - { - id: 'thudm/glm-z1-32b-0414', - provider: 'ppio', - name: 'GLM-Z1 32B', - group: 'thudm' - }, - { - id: 'thudm/glm-z1-9b-0414', - provider: 'ppio', - name: 'GLM-Z1 9B', - group: 'thudm' } ], alayanew: [], diff --git a/src/renderer/src/config/providers.ts b/src/renderer/src/config/providers.ts index 0dc34d8a5e..2de9ec4805 100644 --- a/src/renderer/src/config/providers.ts +++ b/src/renderer/src/config/providers.ts @@ -163,8 +163,9 @@ export const PROVIDER_CONFIG = { url: 'https://api.ppinfra.com/v3/openai' }, websites: { - official: 'https://ppio.cn/user/register?invited_by=JYT9GD&utm_source=github_cherry-studio', - apiKey: 'https://ppio.cn/user/register?invited_by=JYT9GD&utm_source=github_cherry-studio', + official: 'https://ppio.cn/user/register?invited_by=JYT9GD&utm_source=github_cherry-studio&redirect=/', + apiKey: + 'https://ppio.cn/user/register?invited_by=JYT9GD&utm_source=github_cherry-studio&redirect=/settings/key-management', docs: 'https://docs.cherry-ai.com/pre-basic/providers/ppio?invited_by=JYT9GD&utm_source=github_cherry-studio', models: 'https://ppio.cn/model-api/product/llm-api?invited_by=JYT9GD&utm_source=github_cherry-studio' } diff --git a/src/renderer/src/pages/settings/ProviderSettings/ProviderOAuth.tsx b/src/renderer/src/pages/settings/ProviderSettings/ProviderOAuth.tsx index cf6397f1cc..e5d70d4fee 100644 --- a/src/renderer/src/pages/settings/ProviderSettings/ProviderOAuth.tsx +++ b/src/renderer/src/pages/settings/ProviderSettings/ProviderOAuth.tsx @@ -1,4 +1,5 @@ import AiHubMixProviderLogo from '@renderer/assets/images/providers/aihubmix.webp' +import PPIOProviderLogo from '@renderer/assets/images/providers/ppio.png' import SiliconFlowProviderLogo from '@renderer/assets/images/providers/silicon.png' import TokenFluxProviderLogo from '@renderer/assets/images/providers/tokenflux.png' import { HStack } from '@renderer/components/Layout' @@ -21,14 +22,18 @@ interface Props { const PROVIDER_LOGO_MAP = { silicon: SiliconFlowProviderLogo, aihubmix: AiHubMixProviderLogo, + ppio: PPIOProviderLogo, tokenflux: TokenFluxProviderLogo } const ProviderOAuth: FC = ({ provider, setApiKey }) => { const { t } = useTranslation() - const providerWebsite = + let providerWebsite = PROVIDER_CONFIG[provider.id]?.api?.url.replace('https://', '').replace('api.', '') || provider.name + if (provider.id === 'ppio') { + providerWebsite = 'ppio.cn' + } return ( diff --git a/src/renderer/src/services/ProviderService.ts b/src/renderer/src/services/ProviderService.ts index ef973d7265..644f75104c 100644 --- a/src/renderer/src/services/ProviderService.ts +++ b/src/renderer/src/services/ProviderService.ts @@ -16,11 +16,11 @@ export function getProviderName(id: string) { } export function isProviderSupportAuth(provider: Provider) { - const supportProviders = ['silicon', 'aihubmix', 'tokenflux'] + const supportProviders = ['silicon', 'aihubmix', 'ppio', 'tokenflux'] return supportProviders.includes(provider.id) } export function isProviderSupportCharge(provider: Provider) { - const supportProviders = ['silicon', 'aihubmix'] + const supportProviders = ['silicon', 'aihubmix', 'ppio'] return supportProviders.includes(provider.id) } diff --git a/src/renderer/src/store/index.ts b/src/renderer/src/store/index.ts index 3b6cba1040..c04fb46daa 100644 --- a/src/renderer/src/store/index.ts +++ b/src/renderer/src/store/index.ts @@ -50,7 +50,7 @@ const persistedReducer = persistReducer( { key: 'cherry-studio', storage, - version: 117, + version: 118, blacklist: ['runtime', 'messages', 'messageBlocks'], migrate }, diff --git a/src/renderer/src/store/migrate.ts b/src/renderer/src/store/migrate.ts index 56ffa33cbe..de02e4bcc6 100644 --- a/src/renderer/src/store/migrate.ts +++ b/src/renderer/src/store/migrate.ts @@ -1661,12 +1661,30 @@ const migrateConfig = { return state } }, - '117': (state: RootState) => { + '118': (state: RootState) => { try { - updateProvider(state, 'ppio', { - models: SYSTEM_MODELS.ppio, - apiHost: 'https://api.ppinfra.com/v3/openai/' - }) + const ppioProvider = state.llm.providers.find((provider) => provider.id === 'ppio') + const modelsToRemove = [ + 'qwen/qwen-2.5-72b-instruct', + 'qwen/qwen2.5-32b-instruct', + 'meta-llama/llama-3.1-70b-instruct', + 'meta-llama/llama-3.1-8b-instruct', + '01-ai/yi-1.5-34b-chat', + '01-ai/yi-1.5-9b-chat', + 'thudm/glm-z1-32b-0414', + 'thudm/glm-z1-9b-0414' + ] + if (ppioProvider) { + updateProvider(state, 'ppio', { + models: [ + ...ppioProvider.models.filter((model) => !modelsToRemove.includes(model.id)), + ...SYSTEM_MODELS.ppio.filter( + (systemModel) => !ppioProvider.models.some((existingModel) => existingModel.id === systemModel.id) + ) + ], + apiHost: 'https://api.ppinfra.com/v3/openai/' + }) + } return state } catch (error) { return state diff --git a/src/renderer/src/utils/oauth.ts b/src/renderer/src/utils/oauth.ts index 4b3ec09845..a15207f5d7 100644 --- a/src/renderer/src/utils/oauth.ts +++ b/src/renderer/src/utils/oauth.ts @@ -1,4 +1,4 @@ -import { SILICON_CLIENT_ID, TOKENFLUX_HOST } from '@renderer/config/constant' +import { PPIO_APP_SECRET, PPIO_CLIENT_ID, SILICON_CLIENT_ID, TOKENFLUX_HOST } from '@renderer/config/constant' import i18n, { getLanguageCode } from '@renderer/i18n' export const oauthWithSiliconFlow = async (setKey) => { @@ -58,6 +58,81 @@ export const oauthWithAihubmix = async (setKey) => { window.addEventListener('message', messageHandler) } +export const oauthWithPPIO = async (setKey) => { + const redirectUri = 'cherrystudio://' + const authUrl = `https://ppio.cn/oauth/authorize?client_id=${PPIO_CLIENT_ID}&scope=api%20openid&response_type=code&redirect_uri=${encodeURIComponent(redirectUri)}` + + window.open( + authUrl, + 'oauth', + 'width=720,height=720,toolbar=no,location=no,status=no,menubar=no,scrollbars=yes,resizable=yes,alwaysOnTop=yes,alwaysRaised=yes' + ) + + if (!setKey) { + console.log('[PPIO OAuth] No setKey callback provided, returning early') + return + } + + console.log('[PPIO OAuth] Setting up protocol listener') + + return new Promise((resolve, reject) => { + const removeListener = window.api.protocol.onReceiveData(async (data) => { + try { + const url = new URL(data.url) + const params = new URLSearchParams(url.search) + const code = params.get('code') + + if (!code) { + reject(new Error('No authorization code received')) + return + } + + if (!PPIO_APP_SECRET) { + reject( + new Error('PPIO_APP_SECRET not configured. Please set RENDERER_VITE_PPIO_APP_SECRET environment variable.') + ) + return + } + const formData = new URLSearchParams({ + client_id: PPIO_CLIENT_ID, + client_secret: PPIO_APP_SECRET, + code: code, + grant_type: 'authorization_code', + redirect_uri: redirectUri + }) + const tokenResponse = await fetch('https://ppio.cn/oauth/token', { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + body: formData.toString() + }) + + if (!tokenResponse.ok) { + const errorText = await tokenResponse.text() + console.error('[PPIO OAuth] Token exchange failed:', tokenResponse.status, errorText) + throw new Error(`Failed to exchange code for token: ${tokenResponse.status} ${errorText}`) + } + + const tokenData = await tokenResponse.json() + const accessToken = tokenData.access_token + + if (accessToken) { + setKey(accessToken) + resolve(accessToken) + } else { + reject(new Error('No access token received')) + } + } catch (error) { + console.error('[PPIO OAuth] Error processing callback:', error) + reject(error) + } finally { + removeListener() + } + }) + }) +} + export const oauthWithTokenFlux = async () => { const callbackUrl = `${TOKENFLUX_HOST}/auth/callback?redirect_to=/dashboard/api-keys` const resp = await fetch(`${TOKENFLUX_HOST}/api/auth/auth-url?type=login&callback=${callbackUrl}`, {}) @@ -90,6 +165,11 @@ export const providerCharge = async (provider: string) => { url: `https://tokenflux.ai/dashboard/billing`, width: 900, height: 700 + }, + ppio: { + url: 'https://ppio.cn/billing?utm_source=github_cherry-studio', + width: 900, + height: 700 } } @@ -118,6 +198,11 @@ export const providerBills = async (provider: string) => { url: `https://tokenflux.ai/dashboard/billing`, width: 900, height: 700 + }, + ppio: { + url: 'https://ppio.cn/billing/billing-details?utm_source=github_cherry-studio', + width: 900, + height: 700 } }