mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-26 03:31:24 +08:00
refactor(ProviderSettings): add provider key by urlScheme (#7529)
* refactor(ProviderSettings): streamline API key management and enhance user experience - Refactored the handleProvidersProtocolUrl function to simplify API key handling and improve navigation logic. - Updated the useProviders hook to maintain consistency in provider management. - Enhanced the ApiKeyList component with improved state handling and user feedback for API key validation. - Updated localization files to reflect changes in API key management and user interactions. - Improved styling and layout for better visual consistency across provider settings. * fix(ProviderSettings): enhance confirmation modal title with provider name - Updated the confirmation modal title in the ProvidersList component to include the provider's display name, improving clarity for users during API key management. * update info * udpate line * update line * feat(Protocol): add custom protocol handling for Cherry Studio - Introduced a new protocol handler for 'cherrystudio' in the Electron app, allowing the application to respond to custom URL schemes. - Updated the electron-builder configuration to register the 'cherrystudio' protocol. - Enhanced the main application logic to handle incoming protocol URLs effectively, improving user experience when launching the app via custom links. * feat(ProviderSettings): enhance provider data handling with optional fields - Updated the handleProviderAddKey function to accept optional 'name' and 'type' fields for providers, improving flexibility in provider management. - Adjusted the API key handling logic to utilize these new fields, ensuring a more comprehensive provider configuration. - Enhanced the URL schema documentation to reflect the changes in provider data structure. * delete apikeylist * restore apiService * support utf8 * feat(Protocol): improve URL handling for macOS and Windows - Added caching for the URL received when the app is already running on macOS, ensuring it is processed correctly. - Updated the URL processing logic in handleProvidersProtocolUrl to replace characters for proper decoding. - Simplified base64 decoding in ProviderSettings to enhance readability and maintainability. * fix start in macOS * format code * fix(ProviderSettings): validate provider data before adding - Added validation to ensure 'id', 'newApiKey', and 'baseUrl' are present before proceeding with provider addition. - Implemented error handling to notify users of invalid data and redirect them to the provider settings page. * feat(Protocol): enhance URL processing for versioning - Updated the URL handling logic in handleProvidersProtocolUrl to support versioning by extracting the 'v' parameter. - Added logging for version 1 to facilitate future enhancements in handling different protocol versions. - Improved the processing of the 'data' parameter for better compatibility with the updated URL schema. * feat(i18n): add provider API key management translations for Japanese, Russian, and Traditional Chinese - Introduced new translations for API key management features, including confirmation prompts and error messages related to provider API keys. - Enhanced user experience by providing localized strings for adding, updating, and validating API keys across multiple languages. --------- Co-authored-by: rcadmin <rcadmin@rcadmins-MacBook-Pro-4.local>
This commit is contained in:
parent
8355ed2fa5
commit
0218bf6c89
@ -11,6 +11,11 @@ electronLanguages:
|
||||
- en # for macOS
|
||||
directories:
|
||||
buildResources: build
|
||||
|
||||
protocols:
|
||||
- name: Cherry Studio
|
||||
schemes:
|
||||
- cherrystudio
|
||||
files:
|
||||
- '**/*'
|
||||
- '!**/{.vscode,.yarn,.yarn-lock,.github,.cursorrules,.prettierrc}'
|
||||
|
||||
@ -124,19 +124,27 @@ if (!app.requestSingleInstanceLock()) {
|
||||
registerProtocolClient(app)
|
||||
|
||||
// macOS specific: handle protocol when app is already running
|
||||
|
||||
app.on('open-url', (event, url) => {
|
||||
event.preventDefault()
|
||||
handleProtocolUrl(url)
|
||||
})
|
||||
|
||||
const handleOpenUrl = (args: string[]) => {
|
||||
const url = args.find((arg) => arg.startsWith(CHERRY_STUDIO_PROTOCOL + '://'))
|
||||
if (url) handleProtocolUrl(url)
|
||||
}
|
||||
|
||||
// for windows to start with url
|
||||
handleOpenUrl(process.argv)
|
||||
|
||||
// Listen for second instance
|
||||
app.on('second-instance', (_event, argv) => {
|
||||
windowService.showMainWindow()
|
||||
|
||||
// Protocol handler for Windows/Linux
|
||||
// The commandLine is an array of strings where the last item might be the URL
|
||||
const url = argv.find((arg) => arg.startsWith(CHERRY_STUDIO_PROTOCOL + '://'))
|
||||
if (url) handleProtocolUrl(url)
|
||||
handleOpenUrl(argv)
|
||||
})
|
||||
|
||||
app.on('browser-window-created', (_, window) => {
|
||||
|
||||
@ -19,7 +19,7 @@ export function registerProtocolClient(app: Electron.App) {
|
||||
}
|
||||
}
|
||||
|
||||
app.setAsDefaultProtocolClient('cherrystudio')
|
||||
app.setAsDefaultProtocolClient(CHERRY_STUDIO_PROTOCOL)
|
||||
}
|
||||
|
||||
export function handleProtocolUrl(url: string) {
|
||||
|
||||
@ -1,37 +1,47 @@
|
||||
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)
|
||||
export async function handleProvidersProtocolUrl(url: URL) {
|
||||
switch (url.pathname) {
|
||||
case '/api-keys': {
|
||||
// jsonConfig example:
|
||||
// {
|
||||
// "id": "tokenflux",
|
||||
// "baseUrl": "https://tokenflux.ai/v1",
|
||||
// "apiKey": "sk-xxxx"
|
||||
// "apiKey": "sk-xxxx",
|
||||
// "name": "TokenFlux", // optional
|
||||
// "type": "openai" // optional
|
||||
// }
|
||||
// cherrystudio://providers/api-keys?data={base64Encode(JSON.stringify(jsonConfig))}
|
||||
// cherrystudio://providers/api-keys?v=1&data={base64Encode(JSON.stringify(jsonConfig))}
|
||||
|
||||
// replace + and / to _ and - because + and / are processed by URLSearchParams
|
||||
const processedSearch = url.search.replaceAll('+', '_').replaceAll('/', '-')
|
||||
const params = new URLSearchParams(processedSearch)
|
||||
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}')`)
|
||||
}
|
||||
const mainWindow = windowService.getMainWindow()
|
||||
const version = params.get('v')
|
||||
if (version == '1') {
|
||||
// TODO: handle different version
|
||||
Logger.info('handleProvidersProtocolUrl', { data, version })
|
||||
}
|
||||
|
||||
// add check there is window.navigate function in mainWindow
|
||||
if (
|
||||
mainWindow &&
|
||||
!mainWindow.isDestroyed() &&
|
||||
(await mainWindow.webContents.executeJavaScript(`typeof window.navigate === 'function'`))
|
||||
) {
|
||||
mainWindow.webContents.executeJavaScript(`window.navigate('/settings/provider?addProviderData=${data}')`)
|
||||
} else {
|
||||
Logger.error('No data found in URL')
|
||||
setTimeout(() => {
|
||||
handleProvidersProtocolUrl(url)
|
||||
}, 1000)
|
||||
}
|
||||
break
|
||||
}
|
||||
default:
|
||||
console.error(`Unknown MCP protocol URL: ${url}`)
|
||||
Logger.error(`Unknown MCP protocol URL: ${url}`)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { createSelector } from '@reduxjs/toolkit'
|
||||
import store, { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||
import {
|
||||
addModel,
|
||||
addProvider,
|
||||
@ -10,7 +10,6 @@ import {
|
||||
updateProviders
|
||||
} from '@renderer/store/llm'
|
||||
import { Assistant, Model, Provider } from '@renderer/types'
|
||||
import { IpcChannel } from '@shared/IpcChannel'
|
||||
|
||||
import { useDefaultModel } from './useAssistant'
|
||||
|
||||
@ -64,17 +63,3 @@ 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)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@ -1650,6 +1650,15 @@
|
||||
"models.quick_assistant_default_tag": "Default",
|
||||
"models.use_model": "Default Model",
|
||||
"models.use_assistant": "Use Assistant",
|
||||
"models.provider_key_confirm_title": "Add Provider API Key",
|
||||
"models.provider_name": "Provider Name",
|
||||
"models.provider_id": "Provider ID",
|
||||
"models.base_url": "Base URL",
|
||||
"models.api_key": "API Key",
|
||||
"models.provider_key_add_confirm": "Do you want to add the API key for {{provider}}?",
|
||||
"models.provider_key_override_confirm": "{{provider}} already has an API key ({{existingKey}}). Do you want to override it with the new key ({{newKey}})?",
|
||||
"models.provider_key_added": "Successfully added API key for {{provider}}",
|
||||
"models.provider_key_overridden": "Successfully updated API key for {{provider}}",
|
||||
"moresetting": "More Settings",
|
||||
"moresetting.check.confirm": "Confirm Selection",
|
||||
"moresetting.check.warn": "Please be cautious when selecting this option. Incorrect selection may cause the model to malfunction!",
|
||||
|
||||
@ -1638,6 +1638,18 @@
|
||||
"models.quick_assistant_default_tag": "デフォルト",
|
||||
"models.use_model": "デフォルトモデル",
|
||||
"models.use_assistant": "アシスタントの活用",
|
||||
"models.provider_key_confirm_title": "{{provider}} の API キーを追加",
|
||||
"models.provider_name": "プロバイダー名",
|
||||
"models.provider_id": "プロバイダー ID",
|
||||
"models.base_url": "ベース URL",
|
||||
"models.api_key": "API キー",
|
||||
"models.provider_key_add_confirm": "{{provider}} の API キーを追加しますか?",
|
||||
"models.provider_key_already_exists": "{{provider}} には同じ API キーがすでに存在します。追加しません。",
|
||||
"models.provider_key_added": "{{provider}} の API キーを追加しました",
|
||||
"models.provider_key_overridden": "{{provider}} の API キーを更新しました",
|
||||
"models.provider_key_no_change": "{{provider}} の API キーは変更されませんでした",
|
||||
"models.provider_key_add_failed_by_empty_data": "{{provider}} の API キーを追加できませんでした。データが空です。",
|
||||
"models.provider_key_add_failed_by_invalid_data": "{{provider}} の API キーを追加できませんでした。データ形式が無効です。",
|
||||
"moresetting": "詳細設定",
|
||||
"moresetting.check.confirm": "選択を確認",
|
||||
"moresetting.check.warn": "このオプションを選択する際は慎重に行ってください。誤った選択はモデルの誤動作を引き起こす可能性があります!",
|
||||
|
||||
@ -1638,6 +1638,18 @@
|
||||
"models.quick_assistant_default_tag": "умолчанию",
|
||||
"models.use_model": "модель по умолчанию",
|
||||
"models.use_assistant": "Использование ассистентов",
|
||||
"models.provider_key_confirm_title": "Добавить API ключ для {{provider}}",
|
||||
"models.provider_name": "Имя провайдера",
|
||||
"models.provider_id": "ID провайдера",
|
||||
"models.base_url": "Базовый URL",
|
||||
"models.api_key": "API ключ",
|
||||
"models.provider_key_add_confirm": "Добавить API ключ для {{provider}}?",
|
||||
"models.provider_key_already_exists": "{{provider}} уже существует один и тот же API ключ, не будет добавлен",
|
||||
"models.provider_key_added": "API ключ для {{provider}} успешно добавлен",
|
||||
"models.provider_key_overridden": "API ключ для {{provider}} успешно обновлен",
|
||||
"models.provider_key_no_change": "API ключ для {{provider}} не изменился",
|
||||
"models.provider_key_add_failed_by_empty_data": "Не удалось добавить API ключ для {{provider}}, данные пусты",
|
||||
"models.provider_key_add_failed_by_invalid_data": "Не удалось добавить API ключ для {{provider}}, данные имеют неверный формат",
|
||||
"moresetting": "Дополнительные настройки",
|
||||
"moresetting.check.confirm": "Подтвердить выбор",
|
||||
"moresetting.check.warn": "Пожалуйста, будьте осторожны при выборе этой опции. Неправильный выбор может привести к сбою в работе модели!",
|
||||
|
||||
@ -1650,6 +1650,18 @@
|
||||
"models.quick_assistant_default_tag": "默认",
|
||||
"models.use_model": "默认模型",
|
||||
"models.use_assistant": "使用助手",
|
||||
"models.provider_key_confirm_title": "为{{provider}}添加 API 密钥",
|
||||
"models.provider_name": "服务商名称",
|
||||
"models.provider_id": "服务商 ID",
|
||||
"models.base_url": "基础 URL",
|
||||
"models.api_key": "API 密钥",
|
||||
"models.provider_key_add_confirm": "是否要为 {{provider}} 添加 API 密钥?",
|
||||
"models.provider_key_already_exists": "{{provider}} 已存在相同API 密钥, 不会重复添加",
|
||||
"models.provider_key_added": "成功为 {{provider}} 添加 API 密钥",
|
||||
"models.provider_key_overridden": "成功更新 {{provider}} 的 API 密钥",
|
||||
"models.provider_key_no_change": "{{provider}} 的 API 密钥没有变化",
|
||||
"models.provider_key_add_failed_by_empty_data": "添加服务商 API 密钥失败,数据为空",
|
||||
"models.provider_key_add_failed_by_invalid_data": "添加服务商 API 密钥失败,数据格式错误",
|
||||
"moresetting": "更多设置",
|
||||
"moresetting.check.confirm": "确认勾选",
|
||||
"moresetting.check.warn": "请慎重勾选此选项,勾选错误会导致模型无法正常使用!!!",
|
||||
|
||||
@ -1641,6 +1641,18 @@
|
||||
"models.quick_assistant_default_tag": "預設",
|
||||
"models.use_model": "預設模型",
|
||||
"models.use_assistant": "使用助手",
|
||||
"models.provider_key_confirm_title": "為{{provider}}添加 API 密鑰",
|
||||
"models.provider_name": "提供者名稱",
|
||||
"models.provider_id": "提供者 ID",
|
||||
"models.base_url": "基礎 URL",
|
||||
"models.api_key": "API 密鑰",
|
||||
"models.provider_key_add_confirm": "是否要為 {{provider}} 添加 API 密鑰?",
|
||||
"models.provider_key_already_exists": "{{provider}} 已存在相同API 密鑰, 不會重複添加",
|
||||
"models.provider_key_added": "成功為 {{provider}} 添加 API 密鑰",
|
||||
"models.provider_key_overridden": "成功更新 {{provider}} 的 API 密鑰",
|
||||
"models.provider_key_no_change": "{{provider}} 的 API 密鑰沒有變化",
|
||||
"models.provider_key_add_failed_by_empty_data": "添加提供者 API 密鑰失敗,數據為空",
|
||||
"models.provider_key_add_failed_by_invalid_data": "添加提供者 API 密鑰失敗,數據格式錯誤",
|
||||
"moresetting": "更多設定",
|
||||
"moresetting.check.confirm": "確認勾選",
|
||||
"moresetting.check.warn": "請謹慎勾選此選項,勾選錯誤會導致模型無法正常使用!!!",
|
||||
|
||||
@ -5,10 +5,10 @@ import { getProviderLogo } from '@renderer/config/providers'
|
||||
import { useAllProviders, useProviders } from '@renderer/hooks/useProvider'
|
||||
import ImageStorage from '@renderer/services/ImageStorage'
|
||||
import { INITIAL_PROVIDERS } from '@renderer/store/llm'
|
||||
import { Provider } from '@renderer/types'
|
||||
import { Provider, ProviderType } from '@renderer/types'
|
||||
import { droppableReorder, generateColorFromChar, getFirstCharacter, uuid } from '@renderer/utils'
|
||||
import { Avatar, Button, Dropdown, Input, MenuProps, Tag } from 'antd'
|
||||
import { Search, UserPen } from 'lucide-react'
|
||||
import { Avatar, Button, Card, Dropdown, Input, MenuProps, Tag } from 'antd'
|
||||
import { Eye, EyeOff, Search, UserPen } from 'lucide-react'
|
||||
import { FC, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useSearchParams } from 'react-router-dom'
|
||||
@ -61,6 +61,206 @@ const ProvidersList: FC = () => {
|
||||
}
|
||||
}, [providers, searchParams])
|
||||
|
||||
// Handle provider add key from URL schema
|
||||
useEffect(() => {
|
||||
const handleProviderAddKey = (data: {
|
||||
id: string
|
||||
apiKey: string
|
||||
baseUrl: string
|
||||
type?: ProviderType
|
||||
name?: string
|
||||
}) => {
|
||||
const { id, apiKey: newApiKey, baseUrl, type, name } = data
|
||||
|
||||
// 查找匹配的 provider
|
||||
let existingProvider = providers.find((p) => p.id === id)
|
||||
const isNewProvider = !existingProvider
|
||||
|
||||
if (!existingProvider) {
|
||||
existingProvider = {
|
||||
id,
|
||||
name: name || id,
|
||||
type: type || 'openai',
|
||||
apiKey: '',
|
||||
apiHost: baseUrl || '',
|
||||
models: [],
|
||||
enabled: true,
|
||||
isSystem: false
|
||||
}
|
||||
}
|
||||
|
||||
const providerDisplayName = existingProvider.isSystem
|
||||
? t(`provider.${existingProvider.id}`)
|
||||
: existingProvider.name
|
||||
|
||||
// 检查是否已有 API Key
|
||||
const hasExistingKey = existingProvider.apiKey && existingProvider.apiKey.trim() !== ''
|
||||
|
||||
// 检查新的 API Key 是否已经存在
|
||||
const existingKeys = hasExistingKey ? existingProvider.apiKey.split(',').map((k) => k.trim()) : []
|
||||
const keyAlreadyExists = existingKeys.includes(newApiKey.trim())
|
||||
|
||||
const confirmMessage = keyAlreadyExists
|
||||
? t('settings.models.provider_key_already_exists', {
|
||||
provider: providerDisplayName,
|
||||
key: '*********'
|
||||
})
|
||||
: t('settings.models.provider_key_add_confirm', {
|
||||
provider: providerDisplayName,
|
||||
newKey: '*********'
|
||||
})
|
||||
|
||||
const createModalContent = () => {
|
||||
let showApiKey = false
|
||||
|
||||
const toggleApiKey = () => {
|
||||
showApiKey = !showApiKey
|
||||
// 重新渲染模态框内容
|
||||
updateModalContent()
|
||||
}
|
||||
|
||||
const updateModalContent = () => {
|
||||
const content = (
|
||||
<ProviderInfoContainer>
|
||||
<ProviderInfoCard size="small">
|
||||
<ProviderInfoRow>
|
||||
<ProviderInfoLabel>{t('settings.models.provider_name')}:</ProviderInfoLabel>
|
||||
<ProviderInfoValue>{providerDisplayName}</ProviderInfoValue>
|
||||
</ProviderInfoRow>
|
||||
<ProviderInfoRow>
|
||||
<ProviderInfoLabel>{t('settings.models.provider_id')}:</ProviderInfoLabel>
|
||||
<ProviderInfoValue>{id}</ProviderInfoValue>
|
||||
</ProviderInfoRow>
|
||||
{baseUrl && (
|
||||
<ProviderInfoRow>
|
||||
<ProviderInfoLabel>{t('settings.models.base_url')}:</ProviderInfoLabel>
|
||||
<ProviderInfoValue>{baseUrl}</ProviderInfoValue>
|
||||
</ProviderInfoRow>
|
||||
)}
|
||||
<ProviderInfoRow>
|
||||
<ProviderInfoLabel>{t('settings.models.api_key')}:</ProviderInfoLabel>
|
||||
<ApiKeyContainer>
|
||||
<ApiKeyValue>{showApiKey ? newApiKey : '*********'}</ApiKeyValue>
|
||||
<EyeButton onClick={toggleApiKey}>
|
||||
{showApiKey ? <EyeOff size={16} /> : <Eye size={16} />}
|
||||
</EyeButton>
|
||||
</ApiKeyContainer>
|
||||
</ProviderInfoRow>
|
||||
</ProviderInfoCard>
|
||||
<ConfirmMessage>{confirmMessage}</ConfirmMessage>
|
||||
</ProviderInfoContainer>
|
||||
)
|
||||
|
||||
// 更新模态框内容
|
||||
if (modalInstance) {
|
||||
modalInstance.update({
|
||||
content: content
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const modalInstance = window.modal.confirm({
|
||||
title: t('settings.models.provider_key_confirm_title', { provider: providerDisplayName }),
|
||||
content: (
|
||||
<ProviderInfoContainer>
|
||||
<ProviderInfoCard size="small">
|
||||
<ProviderInfoRow>
|
||||
<ProviderInfoLabel>{t('settings.models.provider_name')}:</ProviderInfoLabel>
|
||||
<ProviderInfoValue>{providerDisplayName}</ProviderInfoValue>
|
||||
</ProviderInfoRow>
|
||||
<ProviderInfoRow>
|
||||
<ProviderInfoLabel>{t('settings.models.provider_id')}:</ProviderInfoLabel>
|
||||
<ProviderInfoValue>{id}</ProviderInfoValue>
|
||||
</ProviderInfoRow>
|
||||
{baseUrl && (
|
||||
<ProviderInfoRow>
|
||||
<ProviderInfoLabel>{t('settings.models.base_url')}:</ProviderInfoLabel>
|
||||
<ProviderInfoValue>{baseUrl}</ProviderInfoValue>
|
||||
</ProviderInfoRow>
|
||||
)}
|
||||
<ProviderInfoRow>
|
||||
<ProviderInfoLabel>{t('settings.models.api_key')}:</ProviderInfoLabel>
|
||||
<ApiKeyContainer>
|
||||
<ApiKeyValue>{showApiKey ? newApiKey : '*********'}</ApiKeyValue>
|
||||
<EyeButton onClick={toggleApiKey}>
|
||||
{showApiKey ? <EyeOff size={16} /> : <Eye size={16} />}
|
||||
</EyeButton>
|
||||
</ApiKeyContainer>
|
||||
</ProviderInfoRow>
|
||||
</ProviderInfoCard>
|
||||
<ConfirmMessage>{confirmMessage}</ConfirmMessage>
|
||||
</ProviderInfoContainer>
|
||||
),
|
||||
okText: keyAlreadyExists ? t('common.confirm') : t('common.add'),
|
||||
cancelText: t('common.cancel'),
|
||||
centered: true,
|
||||
onCancel() {
|
||||
window.navigate(`/settings/provider?id=${id}`)
|
||||
},
|
||||
onOk() {
|
||||
window.navigate(`/settings/provider?id=${id}`)
|
||||
if (keyAlreadyExists) {
|
||||
// 如果 key 已经存在,只显示消息,不做任何更改
|
||||
window.message.info(t('settings.models.provider_key_no_change', { provider: providerDisplayName }))
|
||||
return
|
||||
}
|
||||
|
||||
// 如果 key 不存在,添加到现有 keys 的末尾
|
||||
const finalApiKey = hasExistingKey ? `${existingProvider.apiKey},${newApiKey.trim()}` : newApiKey.trim()
|
||||
|
||||
const updatedProvider = {
|
||||
...existingProvider,
|
||||
apiKey: finalApiKey,
|
||||
...(baseUrl && { apiHost: baseUrl })
|
||||
}
|
||||
|
||||
if (isNewProvider) {
|
||||
addProvider(updatedProvider)
|
||||
} else {
|
||||
updateProvider(updatedProvider)
|
||||
}
|
||||
|
||||
setSelectedProvider(updatedProvider)
|
||||
window.message.success(t('settings.models.provider_key_added', { provider: providerDisplayName }))
|
||||
}
|
||||
})
|
||||
|
||||
return modalInstance
|
||||
}
|
||||
|
||||
createModalContent()
|
||||
}
|
||||
|
||||
// 检查 URL 参数
|
||||
const addProviderData = searchParams.get('addProviderData')
|
||||
if (!addProviderData) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const base64Decode = (base64EncodedString: string) =>
|
||||
new TextDecoder().decode(Uint8Array.from(atob(base64EncodedString), (m) => m.charCodeAt(0)))
|
||||
const {
|
||||
id,
|
||||
apiKey: newApiKey,
|
||||
baseUrl,
|
||||
type,
|
||||
name
|
||||
} = JSON.parse(base64Decode(addProviderData.replaceAll('_', '+').replaceAll('-', '/')))
|
||||
|
||||
if (!id || !newApiKey || !baseUrl) {
|
||||
window.message.error(t('settings.models.provider_key_add_failed_by_invalid_data'))
|
||||
window.navigate('/settings/provider')
|
||||
return
|
||||
}
|
||||
|
||||
handleProviderAddKey({ id, apiKey: newApiKey, baseUrl, type, name })
|
||||
} catch (error) {
|
||||
window.message.error(t('settings.models.provider_key_add_failed_by_invalid_data'))
|
||||
window.navigate('/settings/provider')
|
||||
}
|
||||
}, [searchParams])
|
||||
|
||||
const onDragEnd = (result: DropResult) => {
|
||||
setDragging(false)
|
||||
if (result.destination) {
|
||||
@ -380,4 +580,97 @@ const AddButtonWrapper = styled.div`
|
||||
align-items: center;
|
||||
padding: 10px 8px;
|
||||
`
|
||||
|
||||
const ProviderInfoContainer = styled.div`
|
||||
color: var(--color-text);
|
||||
`
|
||||
|
||||
const ProviderInfoCard = styled(Card)`
|
||||
margin-bottom: 16px;
|
||||
background-color: var(--color-background-soft);
|
||||
border: 1px solid var(--color-border);
|
||||
|
||||
.ant-card-body {
|
||||
padding: 12px;
|
||||
}
|
||||
`
|
||||
|
||||
const ProviderInfoRow = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
`
|
||||
|
||||
const ProviderInfoLabel = styled.span`
|
||||
font-weight: 600;
|
||||
color: var(--color-text-2);
|
||||
min-width: 80px;
|
||||
`
|
||||
|
||||
const ProviderInfoValue = styled.span`
|
||||
font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
|
||||
background-color: var(--color-background-soft);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--color-border);
|
||||
word-break: break-all;
|
||||
flex: 1;
|
||||
margin-left: 8px;
|
||||
`
|
||||
|
||||
const ConfirmMessage = styled.div`
|
||||
color: var(--color-text);
|
||||
line-height: 1.5;
|
||||
`
|
||||
|
||||
const ApiKeyContainer = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
margin-left: 8px;
|
||||
position: relative;
|
||||
`
|
||||
|
||||
const ApiKeyValue = styled.span`
|
||||
font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
|
||||
background-color: var(--color-background-soft);
|
||||
padding: 2px 32px 2px 6px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--color-border);
|
||||
word-break: break-all;
|
||||
flex: 1;
|
||||
`
|
||||
|
||||
const EyeButton = styled.button`
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
color: var(--color-text-3);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 4px;
|
||||
border-radius: 2px;
|
||||
transition: all 0.2s ease;
|
||||
position: absolute;
|
||||
right: 4px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
|
||||
&:hover {
|
||||
color: var(--color-text);
|
||||
background-color: var(--color-background-mute);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 2px var(--color-primary-outline);
|
||||
}
|
||||
`
|
||||
|
||||
export default ProvidersList
|
||||
|
||||
Loading…
Reference in New Issue
Block a user