feat: support tokenflux provider (#6358)

* feat: add Cherry Cloud provider with associated assets and localization support

* feat: add Cherry Cloud provider support with OAuth integration and IPC handling

* fix: add success message for Cherry Cloud API key update

* feat: enhance provider navigation with dynamic ID in URL and update selected provider state

* feat: implement Cherry Cloud server synchronization with token management and error handling

* feat: add CherryCloud provider configuration and token management

* feat: integrate TokenFlux provider support and update related configurations

fix: update redux-persist version to 104

refactor: remove redundant tokenflux provider model assignment in migration

fix: update migration to add TokenFlux provider instead of CherryCloud

* feat: enhance TokenFlux server synchronization with API key authentication support

* feat: update TokenFlux provider assets and add new models

* feat: update migration logic for version 106 to add TokenFlux provider

* feat: disable TokenFlux provider by default in INITIAL_PROVIDERS

* feat: add TokenFlux billing URLs to providerCharge and providerBills functions
This commit is contained in:
LiuVaayne 2025-05-23 17:10:07 +08:00 committed by GitHub
parent e5bf6916a6
commit f6d71868cb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 423 additions and 36 deletions

View File

@ -173,5 +173,8 @@ export enum IpcChannel {
StoreSync_Subscribe = 'store-sync:subscribe',
StoreSync_Unsubscribe = 'store-sync:unsubscribe',
StoreSync_OnUpdate = 'store-sync:on-update',
StoreSync_BroadcastSync = 'store-sync:broadcast-sync'
StoreSync_BroadcastSync = 'store-sync:broadcast-sync',
// Provider
Provider_AddKey = 'provider:add-key'
}

View File

@ -6,6 +6,7 @@ import { promisify } from 'node:util'
import { app } from 'electron'
import Logger from 'electron-log'
import { handleProvidersProtocolUrl } from './urlschema/handle-providers'
import { handleMcpProtocolUrl } from './urlschema/mcp-install'
import { windowService } from './WindowService'
@ -34,6 +35,9 @@ export function handleProtocolUrl(url: string) {
case 'mcp':
handleMcpProtocolUrl(urlObj)
return
case 'providers':
handleProvidersProtocolUrl(urlObj)
return
}
// You can send the data to your renderer process

View File

@ -0,0 +1,37 @@
import { IpcChannel } from '@shared/IpcChannel'
import Logger from 'electron-log'
import { windowService } from '../WindowService'
export function handleProvidersProtocolUrl(url: URL) {
const params = new URLSearchParams(url.search)
switch (url.pathname) {
case '/api-keys': {
// jsonConfig example:
// {
// "id": "tokenflux",
// "baseUrl": "https://tokenflux.ai/v1",
// "apiKey": "sk-xxxx"
// }
// cherrystudio://providers/api-keys?data={base64Encode(JSON.stringify(jsonConfig))}
const data = params.get('data')
if (data) {
const stringify = Buffer.from(data, 'base64').toString('utf8')
Logger.info('get api keys from urlschema: ', stringify)
const jsonConfig = JSON.parse(stringify)
Logger.info('get api keys from urlschema: ', jsonConfig)
const mainWindow = windowService.getMainWindow()
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send(IpcChannel.Provider_AddKey, jsonConfig)
mainWindow.webContents.executeJavaScript(`window.navigate('/settings/provider?id=${jsonConfig.id}')`)
}
} else {
Logger.error('No data found in URL')
}
break
}
default:
console.error(`Unknown MCP protocol URL: ${url}`)
break
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -1,5 +1,5 @@
import { Provider } from '@renderer/types'
import { oauthWithAihubmix, oauthWithSiliconFlow } from '@renderer/utils/oauth'
import { oauthWithAihubmix, oauthWithSiliconFlow, oauthWithTokenFlux } from '@renderer/utils/oauth'
import { Button, ButtonProps } from 'antd'
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
@ -27,6 +27,10 @@ const OAuthButton: FC<Props> = ({ provider, onSuccess, ...buttonProps }) => {
if (provider.id === 'aihubmix') {
oauthWithAihubmix(handleSuccess)
}
if (provider.id === 'tokenflux') {
oauthWithTokenFlux()
}
}
return (

View File

@ -10,6 +10,7 @@ export const isWindows = platform === 'win32' || platform === 'win64'
export const isLinux = platform === 'linux'
export const SILICON_CLIENT_ID = 'SFaJLLq0y6CAMoyDm81aMu'
export const TOKENFLUX_HOST = 'https://tokenflux.ai'
// Messages loading configuration
export const INITIAL_MESSAGES_COUNT = 20

View File

@ -34,8 +34,10 @@ import DianxinModelLogo from '@renderer/assets/images/models/dianxin.png'
import DianxinModelLogoDark from '@renderer/assets/images/models/dianxin_dark.png'
import DoubaoModelLogo from '@renderer/assets/images/models/doubao.png'
import DoubaoModelLogoDark from '@renderer/assets/images/models/doubao_dark.png'
import EmbeddingModelLogo from '@renderer/assets/images/models/embedding.png'
import EmbeddingModelLogoDark from '@renderer/assets/images/models/embedding.png'
import {
default as EmbeddingModelLogo,
default as EmbeddingModelLogoDark
} from '@renderer/assets/images/models/embedding.png'
import FlashaudioModelLogo from '@renderer/assets/images/models/flashaudio.png'
import FlashaudioModelLogoDark from '@renderer/assets/images/models/flashaudio_dark.png'
import FluxModelLogo from '@renderer/assets/images/models/flux.png'
@ -44,14 +46,15 @@ import GeminiModelLogo from '@renderer/assets/images/models/gemini.png'
import GeminiModelLogoDark from '@renderer/assets/images/models/gemini_dark.png'
import GemmaModelLogo from '@renderer/assets/images/models/gemma.png'
import GemmaModelLogoDark from '@renderer/assets/images/models/gemma_dark.png'
import GoogleModelLogo from '@renderer/assets/images/models/google.png'
import GoogleModelLogoDark from '@renderer/assets/images/models/google.png'
import { default as GoogleModelLogo, default as GoogleModelLogoDark } from '@renderer/assets/images/models/google.png'
import ChatGPT35ModelLogo from '@renderer/assets/images/models/gpt_3.5.png'
import ChatGPT4ModelLogo from '@renderer/assets/images/models/gpt_4.png'
import ChatGptModelLogoDakr from '@renderer/assets/images/models/gpt_dark.png'
import ChatGPT35ModelLogoDark from '@renderer/assets/images/models/gpt_dark.png'
import ChatGPT4ModelLogoDark from '@renderer/assets/images/models/gpt_dark.png'
import ChatGPTo1ModelLogoDark from '@renderer/assets/images/models/gpt_dark.png'
import {
default as ChatGPT4ModelLogoDark,
default as ChatGPT35ModelLogoDark,
default as ChatGptModelLogoDakr,
default as ChatGPTo1ModelLogoDark
} from '@renderer/assets/images/models/gpt_dark.png'
import ChatGPTo1ModelLogo from '@renderer/assets/images/models/gpt_o1.png'
import GrokModelLogo from '@renderer/assets/images/models/grok.png'
import GrokModelLogoDark from '@renderer/assets/images/models/grok_dark.png'
@ -86,22 +89,28 @@ import MicrosoftModelLogo from '@renderer/assets/images/models/microsoft.png'
import MicrosoftModelLogoDark from '@renderer/assets/images/models/microsoft_dark.png'
import MidjourneyModelLogo from '@renderer/assets/images/models/midjourney.png'
import MidjourneyModelLogoDark from '@renderer/assets/images/models/midjourney_dark.png'
import MinicpmModelLogo from '@renderer/assets/images/models/minicpm.webp'
import MinicpmModelLogoDark from '@renderer/assets/images/models/minicpm.webp'
import {
default as MinicpmModelLogo,
default as MinicpmModelLogoDark
} from '@renderer/assets/images/models/minicpm.webp'
import MinimaxModelLogo from '@renderer/assets/images/models/minimax.png'
import MinimaxModelLogoDark from '@renderer/assets/images/models/minimax_dark.png'
import MistralModelLogo from '@renderer/assets/images/models/mixtral.png'
import MistralModelLogoDark from '@renderer/assets/images/models/mixtral_dark.png'
import MoonshotModelLogo from '@renderer/assets/images/models/moonshot.png'
import MoonshotModelLogoDark from '@renderer/assets/images/models/moonshot_dark.png'
import NousResearchModelLogo from '@renderer/assets/images/models/nousresearch.png'
import NousResearchModelLogoDark from '@renderer/assets/images/models/nousresearch.png'
import {
default as NousResearchModelLogo,
default as NousResearchModelLogoDark
} from '@renderer/assets/images/models/nousresearch.png'
import NvidiaModelLogo from '@renderer/assets/images/models/nvidia.png'
import NvidiaModelLogoDark from '@renderer/assets/images/models/nvidia_dark.png'
import PalmModelLogo from '@renderer/assets/images/models/palm.png'
import PalmModelLogoDark from '@renderer/assets/images/models/palm_dark.png'
import PerplexityModelLogo from '@renderer/assets/images/models/perplexity.png'
import PerplexityModelLogoDark from '@renderer/assets/images/models/perplexity.png'
import {
default as PerplexityModelLogo,
default as PerplexityModelLogoDark
} from '@renderer/assets/images/models/perplexity.png'
import PixtralModelLogo from '@renderer/assets/images/models/pixtral.png'
import PixtralModelLogoDark from '@renderer/assets/images/models/pixtral_dark.png'
import QwenModelLogo from '@renderer/assets/images/models/qwen.png'
@ -118,6 +127,8 @@ import SunoModelLogo from '@renderer/assets/images/models/suno.png'
import SunoModelLogoDark from '@renderer/assets/images/models/suno_dark.png'
import TeleModelLogo from '@renderer/assets/images/models/tele.png'
import TeleModelLogoDark from '@renderer/assets/images/models/tele_dark.png'
import TokenFluxModelLogo from '@renderer/assets/images/models/tokenflux.png'
import TokenFluxModelLogoDark from '@renderer/assets/images/models/tokenflux_dark.png'
import UpstageModelLogo from '@renderer/assets/images/models/upstage.png'
import UpstageModelLogoDark from '@renderer/assets/images/models/upstage_dark.png'
import ViduModelLogo from '@renderer/assets/images/models/vidu.png'
@ -369,7 +380,8 @@ export function getModelLogo(modelId: string) {
perplexity: isLight ? PerplexityModelLogo : PerplexityModelLogoDark,
sonar: isLight ? PerplexityModelLogo : PerplexityModelLogoDark,
'bge-': BgeModelLogo,
'voyage-': VoyageModelLogo
'voyage-': VoyageModelLogo,
tokenflux: isLight ? TokenFluxModelLogo : TokenFluxModelLogoDark
}
for (const key in logoMap) {
@ -2160,6 +2172,68 @@ export const SYSTEM_MODELS: Record<string, Model[]> = {
name: 'Qwen2.5 72B Instruct',
group: 'Qwen'
}
],
tokenflux: [
{
id: 'gpt-4.1',
provider: 'tokenflux',
name: 'GPT-4.1',
group: 'GPT-4.1'
},
{
id: 'gpt-4.1-mini',
provider: 'tokenflux',
name: 'GPT-4.1 Mini',
group: 'GPT-4.1'
},
{
id: 'claude-sonnet-4',
provider: 'tokenflux',
name: 'Claude Sonnet 4',
group: 'Claude'
},
{
id: 'claude-3-7-sonnet',
provider: 'tokenflux',
name: 'Claude 3.7 Sonnet',
group: 'Claude'
},
{
id: 'gemini-2.5-pro',
provider: 'tokenflux',
name: 'Gemini 2.5 Pro',
group: 'Gemini'
},
{
id: 'gemini-2.5-flash',
provider: 'tokenflux',
name: 'Gemini 2.5 Flash',
group: 'Gemini'
},
{
id: 'deepseek-r1',
provider: 'tokenflux',
name: 'DeepSeek R1',
group: 'DeepSeek'
},
{
id: 'deepseek-v3',
provider: 'tokenflux',
name: 'DeepSeek V3',
group: 'DeepSeek'
},
{
id: 'qwen-max',
provider: 'tokenflux',
name: 'Qwen Max',
group: 'Qwen'
},
{
id: 'qwen-plus',
provider: 'tokenflux',
name: 'Qwen Plus',
group: 'Qwen'
}
]
}

View File

@ -38,12 +38,15 @@ import SiliconFlowProviderLogo from '@renderer/assets/images/providers/silicon.p
import StepProviderLogo from '@renderer/assets/images/providers/step.png'
import TencentCloudProviderLogo from '@renderer/assets/images/providers/tencent-cloud-ti.png'
import TogetherProviderLogo from '@renderer/assets/images/providers/together.png'
import TokenFluxProviderLogo from '@renderer/assets/images/providers/tokenflux.png'
import BytedanceProviderLogo from '@renderer/assets/images/providers/volcengine.png'
import VoyageAIProviderLogo from '@renderer/assets/images/providers/voyageai.png'
import XirangProviderLogo from '@renderer/assets/images/providers/xirang.png'
import ZeroOneProviderLogo from '@renderer/assets/images/providers/zero-one.png'
import ZhipuProviderLogo from '@renderer/assets/images/providers/zhipu.png'
import { TOKENFLUX_HOST } from './constant'
const PROVIDER_LOGO_MAP = {
openai: OpenAiProviderLogo,
silicon: SiliconFlowProviderLogo,
@ -90,7 +93,8 @@ const PROVIDER_LOGO_MAP = {
gpustack: GPUStackProviderLogo,
alayanew: AlayaNewProviderLogo,
voyageai: VoyageAIProviderLogo,
qiniu: QiniuProviderLogo
qiniu: QiniuProviderLogo,
tokenflux: TokenFluxProviderLogo
} as const
export function getProviderLogo(providerId: string) {
@ -597,5 +601,16 @@ export const PROVIDER_CONFIG = {
docs: 'https://developer.qiniu.com/aitokenapi',
models: 'https://developer.qiniu.com/aitokenapi/12883/model-list'
}
},
tokenflux: {
api: {
url: TOKENFLUX_HOST
},
websites: {
official: TOKENFLUX_HOST,
apiKey: `${TOKENFLUX_HOST}/dashboard/api-keys`,
docs: `${TOKENFLUX_HOST}/docs`,
models: `${TOKENFLUX_HOST}/models`
}
}
}

View File

@ -1,5 +1,5 @@
import { createSelector } from '@reduxjs/toolkit'
import { useAppDispatch, useAppSelector } from '@renderer/store'
import store, { useAppDispatch, useAppSelector } from '@renderer/store'
import {
addModel,
addProvider,
@ -10,6 +10,7 @@ import {
updateProviders
} from '@renderer/store/llm'
import { Assistant, Model, Provider } from '@renderer/types'
import { IpcChannel } from '@shared/IpcChannel'
import { useDefaultModel } from './useAssistant'
@ -63,3 +64,17 @@ export function useProviderByAssistant(assistant: Assistant) {
const { provider } = useProvider(model.provider)
return provider
}
// Listen for server changes from main process
window.electron.ipcRenderer.on(IpcChannel.Provider_AddKey, (_, data) => {
console.log('Received provider key data:', data)
const { id, apiKey } = data
// for now only suppor tokenflux, but in the future we can support more
if (id === 'tokenflux') {
if (apiKey) {
store.dispatch(updateProvider({ id, apiKey } as Provider))
window.message.success('Provider API key updated')
console.log('Provider API key updated:', apiKey)
}
}
})

View File

@ -939,7 +939,8 @@
"zhinao": "360AI",
"zhipu": "ZHIPU AI",
"voyageai": "Voyage AI",
"qiniu": "Qiniu"
"qiniu": "Qiniu",
"tokenflux": "TokenFlux"
},
"restore": {
"confirm": "Are you sure you want to restore data?",

View File

@ -940,7 +940,8 @@
"zhinao": "360智脳",
"zhipu": "智譜AI",
"voyageai": "Voyage AI",
"qiniu": "七牛云"
"qiniu": "七牛云",
"tokenflux": "TokenFlux"
},
"restore": {
"confirm": "データを復元しますか?",

View File

@ -940,7 +940,8 @@
"zhinao": "360AI",
"zhipu": "ZHIPU AI",
"voyageai": "Voyage AI",
"qiniu": "Qiniu"
"qiniu": "Qiniu",
"tokenflux": "TokenFlux"
},
"restore": {
"confirm": "Вы уверены, что хотите восстановить данные?",

View File

@ -939,7 +939,8 @@
"zhinao": "360智脑",
"zhipu": "智谱AI",
"voyageai": "Voyage AI",
"qiniu": "七牛云"
"qiniu": "七牛云",
"tokenflux": "TokenFlux"
},
"restore": {
"confirm": "确定要恢复数据吗?",

View File

@ -940,7 +940,8 @@
"zhinao": "360 智腦",
"zhipu": "智譜 AI",
"voyageai": "Voyage AI",
"qiniu": "七牛雲"
"qiniu": "七牛雲",
"tokenflux": "TokenFlux"
},
"restore": {
"confirm": "確定要復原資料嗎?",

View File

@ -1,12 +1,13 @@
import { TopView } from '@renderer/components/TopView'
import { useMCPServers } from '@renderer/hooks/useMCPServers'
import { MCPServer } from '@renderer/types'
import type { MCPServer } from '@renderer/types'
import { Button, Form, Input, Modal, Select } from 'antd'
import { useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import { getModelScopeToken, saveModelScopeToken, syncModelScopeServers } from './modelscopeSyncUtils'
import { getTokenFluxToken, saveTokenFluxToken, syncTokenFluxServers, TOKENFLUX_HOST } from './providers/tokenflux'
// Provider configuration interface
interface ProviderConfig {
@ -33,6 +34,17 @@ const providers: ProviderConfig[] = [
getToken: getModelScopeToken,
saveToken: saveModelScopeToken,
syncServers: syncModelScopeServers
},
{
key: 'tokenflux',
name: 'TokenFlux',
description: 'TokenFlux 平台 MCP 服务',
discoverUrl: `${TOKENFLUX_HOST}/mcps`,
apiKeyUrl: `${TOKENFLUX_HOST}/dashboard/api-keys`,
tokenFieldName: 'tokenfluxToken',
getToken: getTokenFluxToken,
saveToken: saveTokenFluxToken,
syncServers: syncTokenFluxServers
}
]
@ -83,7 +95,10 @@ const PopupContainer: React.FC<Props> = ({ resolve, existingServers }) => {
// Save token if present
if (token) {
selectedProvider.saveToken(token)
setTokens((prev) => ({ ...prev, [selectedProvider.tokenFieldName]: token }))
setTokens((prev) => ({
...prev,
[selectedProvider.tokenFieldName]: token
}))
}
// Sync servers
@ -196,11 +211,19 @@ const PopupContainer: React.FC<Props> = ({ resolve, existingServers }) => {
<StepTitle>{t('settings.mcp.sync.setToken', 'Enter Your Token')}</StepTitle>
<Form.Item
name={selectedProvider.tokenFieldName}
rules={[{ required: true, message: t('settings.mcp.sync.tokenRequired', 'API Token is required') }]}>
rules={[
{
required: true,
message: t('settings.mcp.sync.tokenRequired', 'API Token is required')
}
]}>
<Input.Password
placeholder={t('settings.mcp.sync.tokenPlaceholder', 'Enter API token here')}
onChange={(e) => {
setTokens((prev) => ({ ...prev, [selectedProvider.tokenFieldName]: e.target.value }))
setTokens((prev) => ({
...prev,
[selectedProvider.tokenFieldName]: e.target.value
}))
}}
/>
</Form.Item>

View File

@ -0,0 +1,146 @@
import { nanoid } from '@reduxjs/toolkit'
import type { MCPServer } from '@renderer/types'
import i18next from 'i18next'
// Token storage constants and utilities
const TOKEN_STORAGE_KEY = 'tokenflux_token'
export const TOKENFLUX_HOST = 'https://tokenflux.ai'
export const saveTokenFluxToken = (token: string): void => {
localStorage.setItem(TOKEN_STORAGE_KEY, token)
}
export const getTokenFluxToken = (): string | null => {
return localStorage.getItem(TOKEN_STORAGE_KEY)
}
export const clearTokenFluxToken = (): void => {
localStorage.removeItem(TOKEN_STORAGE_KEY)
}
export const hasTokenFluxToken = (): boolean => {
return !!getTokenFluxToken()
}
interface TokenFluxServerAuthSchemaApiKey {
location: string
name: string
prefix: string
}
interface TokenFluxServer {
name: string
display_name?: string
description?: string
version: string
categories?: string[]
logo?: string
security_schemes?: Record<string, unknown>
}
interface TokenFluxSyncResult {
success: boolean
message: string
addedServers: MCPServer[]
errorDetails?: string
}
// Function to fetch and process TokenFlux servers
export const syncTokenFluxServers = async (
token: string,
existingServers: MCPServer[]
): Promise<TokenFluxSyncResult> => {
const t = i18next.t
try {
const response = await fetch(`${TOKENFLUX_HOST}/v1/mcps?enabled=true`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
}
})
// Handle authentication errors
if (response.status === 401 || response.status === 403) {
clearTokenFluxToken()
return {
success: false,
message: t('settings.mcp.sync.unauthorized', 'Sync Unauthorized'),
addedServers: []
}
}
// Handle server errors
if (response.status === 500 || !response.ok) {
return {
success: false,
message: t('settings.mcp.sync.error'),
addedServers: [],
errorDetails: `Status: ${response.status}`
}
}
// Process successful response
const data = await response.json()
const servers: TokenFluxServer[] = data.data || []
if (servers.length === 0) {
return {
success: true,
message: t('settings.mcp.sync.noServersAvailable', 'No MCP servers available'),
addedServers: []
}
}
// Transform TokenFlux servers to MCP servers format
const addedServers: MCPServer[] = []
for (const server of servers) {
try {
// Skip if server already exists
if (existingServers.some((s) => s.id === `@tokenflux/${server.name}`)) continue
const authHeaders = {}
if (server.security_schemes && server.security_schemes.api_key) {
const keyAuth = server.security_schemes.api_key as TokenFluxServerAuthSchemaApiKey
if (keyAuth.location === 'header') {
authHeaders[keyAuth.name] = `${keyAuth.prefix || ''} {set your key here}`.trim()
}
}
const mcpServer: MCPServer = {
id: `@tokenflux/${server.name}`,
name: server.display_name || server.name || `TokenFlux Server ${nanoid()}`,
description: server.description || '',
type: 'streamableHttp',
baseUrl: `${TOKENFLUX_HOST}/v1/mcps/${server.name}`,
isActive: true,
provider: 'TokenFlux',
providerUrl: `${TOKENFLUX_HOST}/mcps/${server.name}`,
logoUrl: server.logo || '',
tags: server.categories || [],
headers: authHeaders
}
addedServers.push(mcpServer)
} catch (err) {
console.error('Error processing TokenFlux server:', err)
}
}
return {
success: true,
message: t('settings.mcp.sync.success', { count: addedServers.length }),
addedServers
}
} catch (error) {
console.error('TokenFlux sync error:', error)
return {
success: false,
message: t('settings.mcp.sync.error'),
addedServers: [],
errorDetails: String(error)
}
}
}

View File

@ -1,5 +1,6 @@
import AiHubMixProviderLogo from '@renderer/assets/images/providers/aihubmix.webp'
import SiliconFlowProviderLogo from '@renderer/assets/images/providers/silicon.png'
import TokenFluxProviderLogo from '@renderer/assets/images/providers/tokenflux.png'
import { HStack } from '@renderer/components/Layout'
import OAuthButton from '@renderer/components/OAuth/OAuthButton'
import { PROVIDER_CONFIG } from '@renderer/config/providers'
@ -7,8 +8,7 @@ import { Provider } from '@renderer/types'
import { providerBills, providerCharge } from '@renderer/utils/oauth'
import { Button } from 'antd'
import { isEmpty } from 'lodash'
import { ReceiptText } from 'lucide-react'
import { CircleDollarSign } from 'lucide-react'
import { CircleDollarSign, ReceiptText } from 'lucide-react'
import { FC } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import styled from 'styled-components'
@ -20,7 +20,8 @@ interface Props {
const PROVIDER_LOGO_MAP = {
silicon: SiliconFlowProviderLogo,
aihubmix: AiHubMixProviderLogo
aihubmix: AiHubMixProviderLogo,
tokenflux: TokenFluxProviderLogo
}
const ProviderOAuth: FC<Props> = ({ provider, setApiKey }) => {

View File

@ -10,6 +10,7 @@ import { Avatar, Button, Dropdown, Input, MenuProps, Tag } from 'antd'
import { Search, UserPen } from 'lucide-react'
import { FC, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useSearchParams } from 'react-router-dom'
import styled from 'styled-components'
import AddProviderPopup from './AddProviderPopup'
@ -17,6 +18,7 @@ import ModelNotesPopup from './ModelNotesPopup'
import ProviderSetting from './ProviderSetting'
const ProvidersList: FC = () => {
const [searchParams] = useSearchParams()
const providers = useAllProviders()
const { updateProviders, addProvider, removeProvider, updateProvider } = useProviders()
const [selectedProvider, setSelectedProvider] = useState<Provider>(providers[0])
@ -46,6 +48,18 @@ const ProvidersList: FC = () => {
loadAllLogos()
}, [providers])
useEffect(() => {
if (searchParams.get('id')) {
const providerId = searchParams.get('id')
const provider = providers.find((p) => p.id === providerId)
if (provider) {
setSelectedProvider(provider)
} else {
setSelectedProvider(providers[0])
}
}
}, [providers, searchParams])
const onDragEnd = (result: DropResult) => {
setDragging(false)
if (result.destination) {

View File

@ -16,7 +16,7 @@ export function getProviderName(id: string) {
}
export function isProviderSupportAuth(provider: Provider) {
const supportProviders = ['silicon', 'aihubmix']
const supportProviders = ['silicon', 'aihubmix', 'tokenflux']
return supportProviders.includes(provider.id)
}

View File

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

View File

@ -487,6 +487,16 @@ export const INITIAL_PROVIDERS: Provider[] = [
models: SYSTEM_MODELS.voyageai,
isSystem: true,
enabled: false
},
{
id: 'tokenflux',
name: 'TokenFlux',
type: 'openai',
apiKey: '',
apiHost: 'https://tokenflux.ai',
models: SYSTEM_MODELS.tokenflux,
isSystem: true,
enabled: false
}
]

View File

@ -1436,6 +1436,15 @@ const migrateConfig = {
} catch (error) {
return state
}
},
'106': (state: RootState) => {
try {
addProvider(state, 'tokenflux')
state.llm.providers = moveProvider(state.llm.providers, 'tokenflux', 15)
return state
} catch (error) {
return state
}
}
}

View File

@ -1,6 +1,6 @@
import { SILICON_CLIENT_ID } from '@renderer/config/constant'
import { getLanguageCode } from '@renderer/i18n'
import i18n from '@renderer/i18n'
import { SILICON_CLIENT_ID, TOKENFLUX_HOST } from '@renderer/config/constant'
import i18n, { getLanguageCode } from '@renderer/i18n'
export const oauthWithSiliconFlow = async (setKey) => {
const authUrl = `https://account.siliconflow.cn/oauth?client_id=${SILICON_CLIENT_ID}`
@ -58,6 +58,22 @@ export const oauthWithAihubmix = async (setKey) => {
window.addEventListener('message', messageHandler)
}
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}`, {})
if (!resp.ok) {
window.message.error(i18n.t('oauth.error'))
return
}
const data = await resp.json()
const authUrl = data.data.url
window.open(
authUrl,
'oauth',
'width=720,height=720,toolbar=no,location=no,status=no,menubar=no,scrollbars=yes,resizable=yes,alwaysOnTop=yes,alwaysRaised=yes'
)
}
export const providerCharge = async (provider: string) => {
const chargeUrlMap = {
silicon: {
@ -69,6 +85,11 @@ export const providerCharge = async (provider: string) => {
url: `https://aihubmix.com/topup?client_id=cherry_studio_oauth&lang=${getLanguageCode()}&aff=SJyh`,
width: 720,
height: 900
},
tokenflux: {
url: `https://tokenflux.ai/dashboard/billing`,
width: 900,
height: 700
}
}
@ -92,6 +113,11 @@ export const providerBills = async (provider: string) => {
url: `https://aihubmix.com/statistics?client_id=cherry_studio_oauth&lang=${getLanguageCode()}&aff=SJyh`,
width: 900,
height: 700
},
tokenflux: {
url: `https://tokenflux.ai/dashboard/billing`,
width: 900,
height: 700
}
}