mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-19 06:30:10 +08:00
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:
parent
e5bf6916a6
commit
f6d71868cb
@ -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'
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
37
src/main/services/urlschema/handle-providers.ts
Normal file
37
src/main/services/urlschema/handle-providers.ts
Normal 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
|
||||
}
|
||||
}
|
||||
BIN
src/renderer/src/assets/images/models/tokenflux.png
Normal file
BIN
src/renderer/src/assets/images/models/tokenflux.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
BIN
src/renderer/src/assets/images/models/tokenflux_dark.png
Normal file
BIN
src/renderer/src/assets/images/models/tokenflux_dark.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
BIN
src/renderer/src/assets/images/providers/tokenflux.png
Normal file
BIN
src/renderer/src/assets/images/providers/tokenflux.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
@ -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 (
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@ -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`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@ -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?",
|
||||
|
||||
@ -940,7 +940,8 @@
|
||||
"zhinao": "360智脳",
|
||||
"zhipu": "智譜AI",
|
||||
"voyageai": "Voyage AI",
|
||||
"qiniu": "七牛云"
|
||||
"qiniu": "七牛云",
|
||||
"tokenflux": "TokenFlux"
|
||||
},
|
||||
"restore": {
|
||||
"confirm": "データを復元しますか?",
|
||||
|
||||
@ -940,7 +940,8 @@
|
||||
"zhinao": "360AI",
|
||||
"zhipu": "ZHIPU AI",
|
||||
"voyageai": "Voyage AI",
|
||||
"qiniu": "Qiniu"
|
||||
"qiniu": "Qiniu",
|
||||
"tokenflux": "TokenFlux"
|
||||
},
|
||||
"restore": {
|
||||
"confirm": "Вы уверены, что хотите восстановить данные?",
|
||||
|
||||
@ -939,7 +939,8 @@
|
||||
"zhinao": "360智脑",
|
||||
"zhipu": "智谱AI",
|
||||
"voyageai": "Voyage AI",
|
||||
"qiniu": "七牛云"
|
||||
"qiniu": "七牛云",
|
||||
"tokenflux": "TokenFlux"
|
||||
},
|
||||
"restore": {
|
||||
"confirm": "确定要恢复数据吗?",
|
||||
|
||||
@ -940,7 +940,8 @@
|
||||
"zhinao": "360 智腦",
|
||||
"zhipu": "智譜 AI",
|
||||
"voyageai": "Voyage AI",
|
||||
"qiniu": "七牛雲"
|
||||
"qiniu": "七牛雲",
|
||||
"tokenflux": "TokenFlux"
|
||||
},
|
||||
"restore": {
|
||||
"confirm": "確定要復原資料嗎?",
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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 }) => {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
|
||||
@ -46,7 +46,7 @@ const persistedReducer = persistReducer(
|
||||
{
|
||||
key: 'cherry-studio',
|
||||
storage,
|
||||
version: 105,
|
||||
version: 106,
|
||||
blacklist: ['runtime', 'messages', 'messageBlocks'],
|
||||
migrate
|
||||
},
|
||||
|
||||
@ -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
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user