From f6d71868cbd96ebc35dffab5ab2377f9ec972a1d Mon Sep 17 00:00:00 2001 From: LiuVaayne <10231735+vaayne@users.noreply.github.com> Date: Fri, 23 May 2025 17:10:07 +0800 Subject: [PATCH] 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 --- packages/shared/IpcChannel.ts | 5 +- src/main/services/ProtocolClient.ts | 4 + .../services/urlschema/handle-providers.ts | 37 +++++ .../src/assets/images/models/tokenflux.png | Bin 0 -> 15114 bytes .../assets/images/models/tokenflux_dark.png | Bin 0 -> 15114 bytes .../src/assets/images/providers/tokenflux.png | Bin 0 -> 15114 bytes .../src/components/OAuth/OAuthButton.tsx | 6 +- src/renderer/src/config/constant.ts | 1 + src/renderer/src/config/models.ts | 104 +++++++++++-- src/renderer/src/config/providers.ts | 17 +- src/renderer/src/hooks/useProvider.ts | 17 +- src/renderer/src/i18n/locales/en-us.json | 3 +- src/renderer/src/i18n/locales/ja-jp.json | 3 +- src/renderer/src/i18n/locales/ru-ru.json | 3 +- src/renderer/src/i18n/locales/zh-cn.json | 3 +- src/renderer/src/i18n/locales/zh-tw.json | 3 +- .../settings/MCPSettings/SyncServersPopup.tsx | 31 +++- .../MCPSettings/providers/tokenflux.ts | 146 ++++++++++++++++++ .../ProviderSettings/ProviderOAuth.tsx | 7 +- .../pages/settings/ProviderSettings/index.tsx | 14 ++ src/renderer/src/services/ProviderService.ts | 2 +- src/renderer/src/store/index.ts | 2 +- src/renderer/src/store/llm.ts | 10 ++ src/renderer/src/store/migrate.ts | 9 ++ src/renderer/src/utils/oauth.ts | 32 +++- 25 files changed, 423 insertions(+), 36 deletions(-) create mode 100644 src/main/services/urlschema/handle-providers.ts create mode 100644 src/renderer/src/assets/images/models/tokenflux.png create mode 100644 src/renderer/src/assets/images/models/tokenflux_dark.png create mode 100644 src/renderer/src/assets/images/providers/tokenflux.png create mode 100644 src/renderer/src/pages/settings/MCPSettings/providers/tokenflux.ts diff --git a/packages/shared/IpcChannel.ts b/packages/shared/IpcChannel.ts index 7ba4164969..e8bd965065 100644 --- a/packages/shared/IpcChannel.ts +++ b/packages/shared/IpcChannel.ts @@ -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' } diff --git a/src/main/services/ProtocolClient.ts b/src/main/services/ProtocolClient.ts index f37c61bb39..7e0b274816 100644 --- a/src/main/services/ProtocolClient.ts +++ b/src/main/services/ProtocolClient.ts @@ -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 diff --git a/src/main/services/urlschema/handle-providers.ts b/src/main/services/urlschema/handle-providers.ts new file mode 100644 index 0000000000..bc109437e6 --- /dev/null +++ b/src/main/services/urlschema/handle-providers.ts @@ -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 + } +} diff --git a/src/renderer/src/assets/images/models/tokenflux.png b/src/renderer/src/assets/images/models/tokenflux.png new file mode 100644 index 0000000000000000000000000000000000000000..e3a8497b6c6f07db22618dab703c8045b3ef1a23 GIT binary patch literal 15114 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 = ({ provider, onSuccess, ...buttonProps }) => { if (provider.id === 'aihubmix') { oauthWithAihubmix(handleSuccess) } + + if (provider.id === 'tokenflux') { + oauthWithTokenFlux() + } } return ( diff --git a/src/renderer/src/config/constant.ts b/src/renderer/src/config/constant.ts index db5f2c9174..6e1021bd43 100644 --- a/src/renderer/src/config/constant.ts +++ b/src/renderer/src/config/constant.ts @@ -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 diff --git a/src/renderer/src/config/models.ts b/src/renderer/src/config/models.ts index 8cb7bf8209..a508b243ed 100644 --- a/src/renderer/src/config/models.ts +++ b/src/renderer/src/config/models.ts @@ -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 = { 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' + } ] } diff --git a/src/renderer/src/config/providers.ts b/src/renderer/src/config/providers.ts index e995e20df0..506eea3ad1 100644 --- a/src/renderer/src/config/providers.ts +++ b/src/renderer/src/config/providers.ts @@ -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` + } } } diff --git a/src/renderer/src/hooks/useProvider.ts b/src/renderer/src/hooks/useProvider.ts index 95a8a8fa0b..0e044602cb 100644 --- a/src/renderer/src/hooks/useProvider.ts +++ b/src/renderer/src/hooks/useProvider.ts @@ -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) + } + } +}) diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index cdd373decc..0c70a0535a 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -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?", diff --git a/src/renderer/src/i18n/locales/ja-jp.json b/src/renderer/src/i18n/locales/ja-jp.json index 4288692c09..0a6d3e9405 100644 --- a/src/renderer/src/i18n/locales/ja-jp.json +++ b/src/renderer/src/i18n/locales/ja-jp.json @@ -940,7 +940,8 @@ "zhinao": "360智脳", "zhipu": "智譜AI", "voyageai": "Voyage AI", - "qiniu": "七牛云" + "qiniu": "七牛云", + "tokenflux": "TokenFlux" }, "restore": { "confirm": "データを復元しますか?", diff --git a/src/renderer/src/i18n/locales/ru-ru.json b/src/renderer/src/i18n/locales/ru-ru.json index 40297a0edc..fc1a54c0d0 100644 --- a/src/renderer/src/i18n/locales/ru-ru.json +++ b/src/renderer/src/i18n/locales/ru-ru.json @@ -940,7 +940,8 @@ "zhinao": "360AI", "zhipu": "ZHIPU AI", "voyageai": "Voyage AI", - "qiniu": "Qiniu" + "qiniu": "Qiniu", + "tokenflux": "TokenFlux" }, "restore": { "confirm": "Вы уверены, что хотите восстановить данные?", diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index 6be1edec07..ce882f6e4c 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -939,7 +939,8 @@ "zhinao": "360智脑", "zhipu": "智谱AI", "voyageai": "Voyage AI", - "qiniu": "七牛云" + "qiniu": "七牛云", + "tokenflux": "TokenFlux" }, "restore": { "confirm": "确定要恢复数据吗?", diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index 88b7145435..cb670a51c9 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -940,7 +940,8 @@ "zhinao": "360 智腦", "zhipu": "智譜 AI", "voyageai": "Voyage AI", - "qiniu": "七牛雲" + "qiniu": "七牛雲", + "tokenflux": "TokenFlux" }, "restore": { "confirm": "確定要復原資料嗎?", diff --git a/src/renderer/src/pages/settings/MCPSettings/SyncServersPopup.tsx b/src/renderer/src/pages/settings/MCPSettings/SyncServersPopup.tsx index 663b593280..f25a1c8735 100644 --- a/src/renderer/src/pages/settings/MCPSettings/SyncServersPopup.tsx +++ b/src/renderer/src/pages/settings/MCPSettings/SyncServersPopup.tsx @@ -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 = ({ 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 = ({ resolve, existingServers }) => { {t('settings.mcp.sync.setToken', 'Enter Your Token')} + rules={[ + { + required: true, + message: t('settings.mcp.sync.tokenRequired', 'API Token is required') + } + ]}> { - setTokens((prev) => ({ ...prev, [selectedProvider.tokenFieldName]: e.target.value })) + setTokens((prev) => ({ + ...prev, + [selectedProvider.tokenFieldName]: e.target.value + })) }} /> diff --git a/src/renderer/src/pages/settings/MCPSettings/providers/tokenflux.ts b/src/renderer/src/pages/settings/MCPSettings/providers/tokenflux.ts new file mode 100644 index 0000000000..7b039cda79 --- /dev/null +++ b/src/renderer/src/pages/settings/MCPSettings/providers/tokenflux.ts @@ -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 +} + +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 => { + 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) + } + } +} diff --git a/src/renderer/src/pages/settings/ProviderSettings/ProviderOAuth.tsx b/src/renderer/src/pages/settings/ProviderSettings/ProviderOAuth.tsx index 2ff44b2876..cf6397f1cc 100644 --- a/src/renderer/src/pages/settings/ProviderSettings/ProviderOAuth.tsx +++ b/src/renderer/src/pages/settings/ProviderSettings/ProviderOAuth.tsx @@ -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 = ({ provider, setApiKey }) => { diff --git a/src/renderer/src/pages/settings/ProviderSettings/index.tsx b/src/renderer/src/pages/settings/ProviderSettings/index.tsx index 4a87b97b36..c382ffa415 100644 --- a/src/renderer/src/pages/settings/ProviderSettings/index.tsx +++ b/src/renderer/src/pages/settings/ProviderSettings/index.tsx @@ -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(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) { diff --git a/src/renderer/src/services/ProviderService.ts b/src/renderer/src/services/ProviderService.ts index 11fe640afd..ef973d7265 100644 --- a/src/renderer/src/services/ProviderService.ts +++ b/src/renderer/src/services/ProviderService.ts @@ -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) } diff --git a/src/renderer/src/store/index.ts b/src/renderer/src/store/index.ts index 851341143a..644ebcccde 100644 --- a/src/renderer/src/store/index.ts +++ b/src/renderer/src/store/index.ts @@ -46,7 +46,7 @@ const persistedReducer = persistReducer( { key: 'cherry-studio', storage, - version: 105, + version: 106, blacklist: ['runtime', 'messages', 'messageBlocks'], migrate }, diff --git a/src/renderer/src/store/llm.ts b/src/renderer/src/store/llm.ts index 31508e5150..e2f2250324 100644 --- a/src/renderer/src/store/llm.ts +++ b/src/renderer/src/store/llm.ts @@ -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 } ] diff --git a/src/renderer/src/store/migrate.ts b/src/renderer/src/store/migrate.ts index 7cdc568d27..65ca22922f 100644 --- a/src/renderer/src/store/migrate.ts +++ b/src/renderer/src/store/migrate.ts @@ -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 + } } } diff --git a/src/renderer/src/utils/oauth.ts b/src/renderer/src/utils/oauth.ts index a96adea572..4b3ec09845 100644 --- a/src/renderer/src/utils/oauth.ts +++ b/src/renderer/src/utils/oauth.ts @@ -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 } }