PPIO OAuth Login (#7717)

* 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 <noreply@anthropic.com>

* fix: fix url

* fix: fix redirect url

* feat: add PPIO OAuth login

* fix: migrate

* fix: migrate

* fix: ppio migrate

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
cnJasonZ 2025-07-02 15:49:37 +08:00 committed by GitHub
parent d5e8ffc00f
commit 561c563bd7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 128 additions and 25 deletions

View File

@ -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<Props> = ({ provider, onSuccess, ...buttonProps }) => {
oauthWithAihubmix(handleSuccess)
}
if (provider.id === 'ppio') {
oauthWithPPIO(handleSuccess)
}
if (provider.id === 'tokenflux') {
oauthWithTokenFlux()
}

View File

@ -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

View File

@ -844,18 +844,6 @@ export const SYSTEM_MODELS: Record<string, Model[]> = {
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: [],

View File

@ -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'
}

View File

@ -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<Props> = ({ 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 (
<Container>

View File

@ -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)
}

View File

@ -50,7 +50,7 @@ const persistedReducer = persistReducer(
{
key: 'cherry-studio',
storage,
version: 117,
version: 118,
blacklist: ['runtime', 'messages', 'messageBlocks'],
migrate
},

View File

@ -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

View File

@ -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<string>((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
}
}