From ffbbec879b1f118eed20e5b39b951f091e518b42 Mon Sep 17 00:00:00 2001 From: one Date: Fri, 29 Aug 2025 15:51:33 +0800 Subject: [PATCH] refactor: provider list and urlSchema popup (#9626) * refactor: separate ProviderList from index * refactor: add UrlSchemaInfoPopup * refactor: improve popup style --- src/renderer/src/i18n/locales/zh-cn.json | 4 +- src/renderer/src/i18n/locales/zh-tw.json | 4 +- .../{index.tsx => ProviderList.tsx} | 270 ++---------------- .../ProviderSettings/UrlSchemaInfoPopup.tsx | 165 +++++++++++ .../pages/settings/ProviderSettings/index.ts | 1 + .../src/pages/settings/SettingsPage.tsx | 4 +- 6 files changed, 190 insertions(+), 258 deletions(-) rename src/renderer/src/pages/settings/ProviderSettings/{index.tsx => ProviderList.tsx} (62%) create mode 100644 src/renderer/src/pages/settings/ProviderSettings/UrlSchemaInfoPopup.tsx create mode 100644 src/renderer/src/pages/settings/ProviderSettings/index.ts diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index c7f948ce72..794ed78bc5 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -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": "快捷助手模型", diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index 50956a8a3a..8622b64b3a 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -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": "快捷助手模型", diff --git a/src/renderer/src/pages/settings/ProviderSettings/index.tsx b/src/renderer/src/pages/settings/ProviderSettings/ProviderList.tsx similarity index 62% rename from src/renderer/src/pages/settings/ProviderSettings/index.tsx rename to src/renderer/src/pages/settings/ProviderSettings/ProviderList.tsx index 9982d35c2e..0de336407d 100644 --- a/src/renderer/src/pages/settings/ProviderSettings/index.tsx +++ b/src/renderer/src/pages/settings/ProviderSettings/ProviderList.tsx @@ -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 = ( - - - - {t('settings.models.provider_name')}: - {providerDisplayName} - - - {t('settings.models.provider_id')}: - {id} - - {baseUrl && ( - - {t('settings.models.base_url')}: - {baseUrl} - - )} - - {t('settings.models.api_key')}: - - {showApiKey ? newApiKey : '*********'} - - {showApiKey ? : } - - - - - {confirmMessage} - - ) - - // 更新模态框内容 - if (modalInstance) { - modalInstance.update({ - content: content - }) - } - } - - const modalInstance = window.modal.confirm({ - title: t('settings.models.provider_key_confirm_title', { provider: providerDisplayName }), - content: ( - - - - {t('settings.models.provider_name')}: - {providerDisplayName} - - - {t('settings.models.provider_id')}: - {id} - - {baseUrl && ( - - {t('settings.models.base_url')}: - {baseUrl} - - )} - - {t('settings.models.api_key')}: - - {showApiKey ? newApiKey : '*********'} - - {showApiKey ? : } - - - - - {confirmMessage} - - ), - 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 diff --git a/src/renderer/src/pages/settings/ProviderSettings/UrlSchemaInfoPopup.tsx b/src/renderer/src/pages/settings/ProviderSettings/UrlSchemaInfoPopup.tsx new file mode 100644 index 0000000000..77a06b222b --- /dev/null +++ b/src/renderer/src/pages/settings/ProviderSettings/UrlSchemaInfoPopup.tsx @@ -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 ( + + + + {displayName} + {baseProvider.id} + {baseUrl && {baseUrl}} + + + {showFullKey ? newApiKey : maskApiKey(newApiKey)} +