mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-10 15:49:29 +08:00
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:
parent
d5e8ffc00f
commit
561c563bd7
@ -1,5 +1,5 @@
|
|||||||
import { Provider } from '@renderer/types'
|
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 { Button, ButtonProps } from 'antd'
|
||||||
import { FC } from 'react'
|
import { FC } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
@ -28,6 +28,10 @@ const OAuthButton: FC<Props> = ({ provider, onSuccess, ...buttonProps }) => {
|
|||||||
oauthWithAihubmix(handleSuccess)
|
oauthWithAihubmix(handleSuccess)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (provider.id === 'ppio') {
|
||||||
|
oauthWithPPIO(handleSuccess)
|
||||||
|
}
|
||||||
|
|
||||||
if (provider.id === 'tokenflux') {
|
if (provider.id === 'tokenflux') {
|
||||||
oauthWithTokenFlux()
|
oauthWithTokenFlux()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,6 +11,8 @@ export const isWin = platform === 'win32' || platform === 'win64'
|
|||||||
export const isLinux = platform === 'linux'
|
export const isLinux = platform === 'linux'
|
||||||
|
|
||||||
export const SILICON_CLIENT_ID = 'SFaJLLq0y6CAMoyDm81aMu'
|
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'
|
export const TOKENFLUX_HOST = 'https://tokenflux.ai'
|
||||||
|
|
||||||
// Messages loading configuration
|
// Messages loading configuration
|
||||||
|
|||||||
@ -844,18 +844,6 @@ export const SYSTEM_MODELS: Record<string, Model[]> = {
|
|||||||
provider: 'ppio',
|
provider: 'ppio',
|
||||||
name: 'Qwen3 Reranker 8B',
|
name: 'Qwen3 Reranker 8B',
|
||||||
group: 'qwen'
|
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: [],
|
alayanew: [],
|
||||||
|
|||||||
@ -163,8 +163,9 @@ export const PROVIDER_CONFIG = {
|
|||||||
url: 'https://api.ppinfra.com/v3/openai'
|
url: 'https://api.ppinfra.com/v3/openai'
|
||||||
},
|
},
|
||||||
websites: {
|
websites: {
|
||||||
official: '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',
|
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',
|
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'
|
models: 'https://ppio.cn/model-api/product/llm-api?invited_by=JYT9GD&utm_source=github_cherry-studio'
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import AiHubMixProviderLogo from '@renderer/assets/images/providers/aihubmix.webp'
|
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 SiliconFlowProviderLogo from '@renderer/assets/images/providers/silicon.png'
|
||||||
import TokenFluxProviderLogo from '@renderer/assets/images/providers/tokenflux.png'
|
import TokenFluxProviderLogo from '@renderer/assets/images/providers/tokenflux.png'
|
||||||
import { HStack } from '@renderer/components/Layout'
|
import { HStack } from '@renderer/components/Layout'
|
||||||
@ -21,14 +22,18 @@ interface Props {
|
|||||||
const PROVIDER_LOGO_MAP = {
|
const PROVIDER_LOGO_MAP = {
|
||||||
silicon: SiliconFlowProviderLogo,
|
silicon: SiliconFlowProviderLogo,
|
||||||
aihubmix: AiHubMixProviderLogo,
|
aihubmix: AiHubMixProviderLogo,
|
||||||
|
ppio: PPIOProviderLogo,
|
||||||
tokenflux: TokenFluxProviderLogo
|
tokenflux: TokenFluxProviderLogo
|
||||||
}
|
}
|
||||||
|
|
||||||
const ProviderOAuth: FC<Props> = ({ provider, setApiKey }) => {
|
const ProviderOAuth: FC<Props> = ({ provider, setApiKey }) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
const providerWebsite =
|
let providerWebsite =
|
||||||
PROVIDER_CONFIG[provider.id]?.api?.url.replace('https://', '').replace('api.', '') || provider.name
|
PROVIDER_CONFIG[provider.id]?.api?.url.replace('https://', '').replace('api.', '') || provider.name
|
||||||
|
if (provider.id === 'ppio') {
|
||||||
|
providerWebsite = 'ppio.cn'
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
|
|||||||
@ -16,11 +16,11 @@ export function getProviderName(id: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function isProviderSupportAuth(provider: Provider) {
|
export function isProviderSupportAuth(provider: Provider) {
|
||||||
const supportProviders = ['silicon', 'aihubmix', 'tokenflux']
|
const supportProviders = ['silicon', 'aihubmix', 'ppio', 'tokenflux']
|
||||||
return supportProviders.includes(provider.id)
|
return supportProviders.includes(provider.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isProviderSupportCharge(provider: Provider) {
|
export function isProviderSupportCharge(provider: Provider) {
|
||||||
const supportProviders = ['silicon', 'aihubmix']
|
const supportProviders = ['silicon', 'aihubmix', 'ppio']
|
||||||
return supportProviders.includes(provider.id)
|
return supportProviders.includes(provider.id)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -50,7 +50,7 @@ const persistedReducer = persistReducer(
|
|||||||
{
|
{
|
||||||
key: 'cherry-studio',
|
key: 'cherry-studio',
|
||||||
storage,
|
storage,
|
||||||
version: 117,
|
version: 118,
|
||||||
blacklist: ['runtime', 'messages', 'messageBlocks'],
|
blacklist: ['runtime', 'messages', 'messageBlocks'],
|
||||||
migrate
|
migrate
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1661,12 +1661,30 @@ const migrateConfig = {
|
|||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'117': (state: RootState) => {
|
'118': (state: RootState) => {
|
||||||
try {
|
try {
|
||||||
updateProvider(state, 'ppio', {
|
const ppioProvider = state.llm.providers.find((provider) => provider.id === 'ppio')
|
||||||
models: SYSTEM_MODELS.ppio,
|
const modelsToRemove = [
|
||||||
apiHost: 'https://api.ppinfra.com/v3/openai/'
|
'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
|
return state
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return state
|
return state
|
||||||
|
|||||||
@ -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'
|
import i18n, { getLanguageCode } from '@renderer/i18n'
|
||||||
|
|
||||||
export const oauthWithSiliconFlow = async (setKey) => {
|
export const oauthWithSiliconFlow = async (setKey) => {
|
||||||
@ -58,6 +58,81 @@ export const oauthWithAihubmix = async (setKey) => {
|
|||||||
window.addEventListener('message', messageHandler)
|
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 () => {
|
export const oauthWithTokenFlux = async () => {
|
||||||
const callbackUrl = `${TOKENFLUX_HOST}/auth/callback?redirect_to=/dashboard/api-keys`
|
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}`, {})
|
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`,
|
url: `https://tokenflux.ai/dashboard/billing`,
|
||||||
width: 900,
|
width: 900,
|
||||||
height: 700
|
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`,
|
url: `https://tokenflux.ai/dashboard/billing`,
|
||||||
width: 900,
|
width: 900,
|
||||||
height: 700
|
height: 700
|
||||||
|
},
|
||||||
|
ppio: {
|
||||||
|
url: 'https://ppio.cn/billing/billing-details?utm_source=github_cherry-studio',
|
||||||
|
width: 900,
|
||||||
|
height: 700
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user