refactor: provider list and urlSchema popup (#9626)

* refactor: separate ProviderList from index

* refactor: add UrlSchemaInfoPopup

* refactor: improve popup style
This commit is contained in:
one 2025-08-29 15:51:33 +08:00 committed by GitHub
parent e5416827cb
commit ffbbec879b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 190 additions and 258 deletions

View File

@ -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": "快捷助手模型",

View File

@ -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": "快捷助手模型",

View File

@ -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

View File

@ -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
)
})
}
}

View File

@ -0,0 +1 @@
export { default as ProviderList } from './ProviderList'

View File

@ -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 />} />