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)}
+
+ ) : (
+
+ )
+ }
+ onClick={() => setShowFullKey((prev) => !prev)}
+ />
+
+
+
+ {confirmMessage}
+
+
+ )
+}
+
+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((resolve) => {
+ TopView.show(
+ {
+ resolve(v)
+ this.hide()
+ }}
+ />,
+ TopViewKey
+ )
+ })
+ }
+}
diff --git a/src/renderer/src/pages/settings/ProviderSettings/index.ts b/src/renderer/src/pages/settings/ProviderSettings/index.ts
new file mode 100644
index 0000000000..85e9a9d279
--- /dev/null
+++ b/src/renderer/src/pages/settings/ProviderSettings/index.ts
@@ -0,0 +1 @@
+export { default as ProviderList } from './ProviderList'
diff --git a/src/renderer/src/pages/settings/SettingsPage.tsx b/src/renderer/src/pages/settings/SettingsPage.tsx
index b8666a8f7d..1f7f8d7a85 100644
--- a/src/renderer/src/pages/settings/SettingsPage.tsx
+++ b/src/renderer/src/pages/settings/SettingsPage.tsx
@@ -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 = () => {
- } />
+ } />
} />
} />
} />