mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-01 17:59:09 +08:00
refactor: provider list and urlSchema popup (#9626)
* refactor: separate ProviderList from index * refactor: add UrlSchemaInfoPopup * refactor: improve popup style
This commit is contained in:
parent
e5416827cb
commit
ffbbec879b
@ -3196,11 +3196,11 @@
|
||||
"provider_key_add_failed_by_empty_data": "添加服务商 API 密钥失败,数据为空",
|
||||
"provider_key_add_failed_by_invalid_data": "添加服务商 API 密钥失败,数据格式错误",
|
||||
"provider_key_added": "成功为 {{provider}} 添加 API 密钥",
|
||||
"provider_key_already_exists": "{{provider}} 已存在相同API 密钥, 不会重复添加",
|
||||
"provider_key_already_exists": "{{provider}} 已存在相同API 密钥,不会重复添加",
|
||||
"provider_key_confirm_title": "为{{provider}}添加 API 密钥",
|
||||
"provider_key_no_change": "{{provider}} 的 API 密钥没有变化",
|
||||
"provider_key_overridden": "成功更新 {{provider}} 的 API 密钥",
|
||||
"provider_key_override_confirm": "{{provider}} 已存在相同 API 密钥, 是否覆盖?",
|
||||
"provider_key_override_confirm": "{{provider}} 已存在相同 API 密钥,是否覆盖?",
|
||||
"provider_name": "服务商名称",
|
||||
"quick_assistant_default_tag": "默认",
|
||||
"quick_assistant_model": "快捷助手模型",
|
||||
|
||||
@ -3196,11 +3196,11 @@
|
||||
"provider_key_add_failed_by_empty_data": "添加提供者 API 密鑰失敗,數據為空",
|
||||
"provider_key_add_failed_by_invalid_data": "添加提供者 API 密鑰失敗,數據格式錯誤",
|
||||
"provider_key_added": "成功為 {{provider}} 添加 API 密鑰",
|
||||
"provider_key_already_exists": "{{provider}} 已存在相同API 密鑰, 不會重複添加",
|
||||
"provider_key_already_exists": "{{provider}} 已存在相同API 密鑰,不會重複添加",
|
||||
"provider_key_confirm_title": "為{{provider}}添加 API 密鑰",
|
||||
"provider_key_no_change": "{{provider}} 的 API 密鑰沒有變化",
|
||||
"provider_key_overridden": "成功更新 {{provider}} 的 API 密鑰",
|
||||
"provider_key_override_confirm": "{{provider}} 已存在相同 API 金鑰, 是否覆蓋?",
|
||||
"provider_key_override_confirm": "{{provider}} 已存在相同 API 金鑰,是否覆蓋?",
|
||||
"provider_name": "提供者名稱",
|
||||
"quick_assistant_default_tag": "預設",
|
||||
"quick_assistant_model": "快捷助手模型",
|
||||
|
||||
@ -9,7 +9,6 @@ import { DeleteIcon, EditIcon, PoeLogo } from '@renderer/components/Icons'
|
||||
import { getProviderLogo } from '@renderer/config/providers'
|
||||
import { useAllProviders, useProviders } from '@renderer/hooks/useProvider'
|
||||
import { useTimer } from '@renderer/hooks/useTimer'
|
||||
import { getProviderLabel } from '@renderer/i18n/label'
|
||||
import ImageStorage from '@renderer/services/ImageStorage'
|
||||
import { isSystemProvider, Provider, ProviderType } from '@renderer/types'
|
||||
import {
|
||||
@ -21,8 +20,8 @@ import {
|
||||
matchKeywordsInProvider,
|
||||
uuid
|
||||
} from '@renderer/utils'
|
||||
import { Avatar, Button, Card, Dropdown, Input, MenuProps, Tag } from 'antd'
|
||||
import { Eye, EyeOff, GripVertical, PlusIcon, Search, UserPen } from 'lucide-react'
|
||||
import { Avatar, Button, Dropdown, Input, MenuProps, Tag } from 'antd'
|
||||
import { GripVertical, PlusIcon, Search, UserPen } from 'lucide-react'
|
||||
import { FC, startTransition, useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useSearchParams } from 'react-router-dom'
|
||||
@ -31,12 +30,13 @@ import styled from 'styled-components'
|
||||
import AddProviderPopup from './AddProviderPopup'
|
||||
import ModelNotesPopup from './ModelNotesPopup'
|
||||
import ProviderSetting from './ProviderSetting'
|
||||
import UrlSchemaInfoPopup from './UrlSchemaInfoPopup'
|
||||
|
||||
const logger = loggerService.withContext('ProvidersList')
|
||||
const logger = loggerService.withContext('ProviderList')
|
||||
|
||||
const BUTTON_WRAPPER_HEIGHT = 50
|
||||
|
||||
const ProvidersList: FC = () => {
|
||||
const ProviderList: FC = () => {
|
||||
const [searchParams] = useSearchParams()
|
||||
const providers = useAllProviders()
|
||||
const { updateProviders, addProvider, removeProvider, updateProvider } = useProviders()
|
||||
@ -99,172 +99,30 @@ const ProvidersList: FC = () => {
|
||||
|
||||
// Handle provider add key from URL schema
|
||||
useEffect(() => {
|
||||
const handleProviderAddKey = (data: {
|
||||
const handleProviderAddKey = async (data: {
|
||||
id: string
|
||||
apiKey: string
|
||||
baseUrl: string
|
||||
type?: ProviderType
|
||||
name?: string
|
||||
}) => {
|
||||
const { id, apiKey: newApiKey, baseUrl, type, name } = data
|
||||
const { id } = data
|
||||
|
||||
// 查找匹配的 provider
|
||||
let existingProvider = providers.find((p) => p.id === id)
|
||||
const isNewProvider = !existingProvider
|
||||
const { updatedProvider, isNew, displayName } = await UrlSchemaInfoPopup.show(data)
|
||||
window.navigate(`/settings/provider?id=${id}`)
|
||||
|
||||
if (!existingProvider) {
|
||||
existingProvider = {
|
||||
id,
|
||||
name: name || id,
|
||||
type: type || 'openai',
|
||||
apiKey: '',
|
||||
apiHost: baseUrl || '',
|
||||
models: [],
|
||||
enabled: true,
|
||||
isSystem: false
|
||||
}
|
||||
if (!updatedProvider) {
|
||||
return
|
||||
}
|
||||
|
||||
const providerDisplayName = isSystemProvider(existingProvider)
|
||||
? getProviderLabel(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
|
||||
if (isNew) {
|
||||
addProvider(updatedProvider)
|
||||
} else {
|
||||
updateProvider(updatedProvider)
|
||||
}
|
||||
|
||||
createModalContent()
|
||||
setSelectedProvider(updatedProvider)
|
||||
window.message.success(t('settings.models.provider_key_added', { provider: displayName }))
|
||||
}
|
||||
|
||||
// 检查 URL 参数
|
||||
@ -626,96 +484,4 @@ const AddButtonWrapper = styled.div`
|
||||
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
|
||||
export default ProviderList
|
||||
@ -0,0 +1,165 @@
|
||||
import { TopView } from '@renderer/components/TopView'
|
||||
import { useAllProviders } from '@renderer/hooks/useProvider'
|
||||
import { Provider, ProviderType } from '@renderer/types'
|
||||
import { getFancyProviderName, maskApiKey } from '@renderer/utils'
|
||||
import { Button, Descriptions, Flex, Modal } from 'antd'
|
||||
import { Eye, EyeOff } from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
interface ShowParams {
|
||||
id: string
|
||||
apiKey: string
|
||||
baseUrl: string
|
||||
type?: ProviderType
|
||||
name?: string
|
||||
}
|
||||
|
||||
interface PopupResult {
|
||||
updatedProvider?: Provider
|
||||
isNew: boolean
|
||||
displayName: string
|
||||
}
|
||||
|
||||
interface Props extends ShowParams {
|
||||
resolve: (result: PopupResult) => void
|
||||
}
|
||||
|
||||
const PopupContainer = ({ id, apiKey: newApiKey, baseUrl, type, name, resolve }: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const providers = useAllProviders()
|
||||
const [open, setOpen] = useState(true)
|
||||
const [showFullKey, setShowFullKey] = useState(false)
|
||||
|
||||
const foundProvider = providers.find((p) => p.id === id)
|
||||
const baseProvider: Provider = foundProvider ?? {
|
||||
id,
|
||||
name: name || id,
|
||||
type: type || 'openai',
|
||||
apiKey: '',
|
||||
apiHost: baseUrl || '',
|
||||
models: [],
|
||||
enabled: true,
|
||||
isSystem: false
|
||||
}
|
||||
|
||||
const displayName = getFancyProviderName(baseProvider)
|
||||
const hasExistingKey = baseProvider.apiKey && baseProvider.apiKey.trim() !== ''
|
||||
const existingKeys = hasExistingKey
|
||||
? baseProvider.apiKey
|
||||
.split(',')
|
||||
.map((k) => k.trim())
|
||||
.filter(Boolean)
|
||||
: []
|
||||
const trimmedNewKey = newApiKey.trim()
|
||||
const keyAlreadyExists = existingKeys.includes(trimmedNewKey)
|
||||
const baseUrlChanged = Boolean(baseUrl) && baseUrl !== baseProvider.apiHost
|
||||
const okDisabled = keyAlreadyExists && !baseUrlChanged
|
||||
|
||||
const confirmMessage = keyAlreadyExists
|
||||
? t('settings.models.provider_key_already_exists', { provider: displayName })
|
||||
: t('settings.models.provider_key_add_confirm', { provider: displayName })
|
||||
|
||||
const okText = keyAlreadyExists ? t('common.confirm') : t('common.add')
|
||||
|
||||
const handleOk = () => {
|
||||
setOpen(false)
|
||||
const finalApiKey = keyAlreadyExists
|
||||
? baseProvider.apiKey
|
||||
: hasExistingKey
|
||||
? `${baseProvider.apiKey},${trimmedNewKey}`
|
||||
: trimmedNewKey
|
||||
const finalApiHost = baseUrlChanged ? baseUrl : baseProvider.apiHost
|
||||
|
||||
if (finalApiKey === baseProvider.apiKey && finalApiHost === baseProvider.apiHost) {
|
||||
resolve({ updatedProvider: undefined, isNew: !foundProvider, displayName })
|
||||
return
|
||||
}
|
||||
|
||||
const updatedProvider: Provider = {
|
||||
...baseProvider,
|
||||
apiKey: finalApiKey,
|
||||
apiHost: finalApiHost
|
||||
}
|
||||
resolve({ updatedProvider, isNew: !foundProvider, displayName })
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
setOpen(false)
|
||||
resolve({ updatedProvider: undefined, isNew: !foundProvider, displayName })
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={t('settings.models.provider_key_confirm_title', { provider: displayName })}
|
||||
open={open}
|
||||
onOk={handleOk}
|
||||
onCancel={handleCancel}
|
||||
okText={okText}
|
||||
okButtonProps={{ disabled: okDisabled }}
|
||||
cancelText={t('common.cancel')}
|
||||
width={500}
|
||||
transitionName="animation-move-down"
|
||||
centered>
|
||||
<Container>
|
||||
<Descriptions column={1} size="small" bordered>
|
||||
<Descriptions.Item label={t('settings.models.provider_name')}>{displayName}</Descriptions.Item>
|
||||
<Descriptions.Item label={t('settings.models.provider_id')}>{baseProvider.id}</Descriptions.Item>
|
||||
{baseUrl && <Descriptions.Item label={t('settings.models.base_url')}>{baseUrl}</Descriptions.Item>}
|
||||
<Descriptions.Item label={t('settings.models.api_key')}>
|
||||
<Flex justify="space-between">
|
||||
{showFullKey ? newApiKey : maskApiKey(newApiKey)}
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
icon={
|
||||
showFullKey ? (
|
||||
<Eye size={16} color="var(--color-text-3)" />
|
||||
) : (
|
||||
<EyeOff size={16} color="var(--color-text-3)" />
|
||||
)
|
||||
}
|
||||
onClick={() => setShowFullKey((prev) => !prev)}
|
||||
/>
|
||||
</Flex>
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
<ConfirmMessage>{confirmMessage}</ConfirmMessage>
|
||||
</Container>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
margin-top: 12px;
|
||||
margin-bottom: 12px;
|
||||
`
|
||||
|
||||
const ConfirmMessage = styled.div`
|
||||
color: var(--color-text);
|
||||
margin-top: 16px;
|
||||
`
|
||||
|
||||
const TopViewKey = 'UrlSchemaInfoPopup'
|
||||
|
||||
export default class UrlSchemaInfoPopup {
|
||||
static topviewId = 0
|
||||
static hide() {
|
||||
TopView.hide(TopViewKey)
|
||||
}
|
||||
static show(props: ShowParams) {
|
||||
return new Promise<PopupResult>((resolve) => {
|
||||
TopView.show(
|
||||
<PopupContainer
|
||||
{...props}
|
||||
resolve={(v) => {
|
||||
resolve(v)
|
||||
this.hide()
|
||||
}}
|
||||
/>,
|
||||
TopViewKey
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
export { default as ProviderList } from './ProviderList'
|
||||
@ -30,7 +30,7 @@ import DocProcessSettings from './DocProcessSettings'
|
||||
import GeneralSettings from './GeneralSettings'
|
||||
import MCPSettings from './MCPSettings'
|
||||
import MemorySettings from './MemorySettings'
|
||||
import ProvidersList from './ProviderSettings'
|
||||
import { ProviderList } from './ProviderSettings'
|
||||
import QuickAssistantSettings from './QuickAssistantSettings'
|
||||
import QuickPhraseSettings from './QuickPhraseSettings'
|
||||
import SelectionAssistantSettings from './SelectionAssistantSettings/SelectionAssistantSettings'
|
||||
@ -141,7 +141,7 @@ const SettingsPage: FC = () => {
|
||||
</SettingMenus>
|
||||
<SettingContent>
|
||||
<Routes>
|
||||
<Route path="provider" element={<ProvidersList />} />
|
||||
<Route path="provider" element={<ProviderList />} />
|
||||
<Route path="model" element={<ModelSettings />} />
|
||||
<Route path="websearch" element={<WebSearchSettings />} />
|
||||
<Route path="docprocess" element={<DocProcessSettings />} />
|
||||
|
||||
Loading…
Reference in New Issue
Block a user