feat: enhance OAuth provider settings with new functionality

- Added a new ProviderOAuth component to manage OAuth authentication and billing for providers.
- Updated existing components to integrate the new ProviderOAuth functionality.
- Enhanced internationalization support for OAuth-related texts across multiple languages.
- Introduced new utility functions for handling provider billing.
This commit is contained in:
kangfenmao 2025-04-25 16:55:36 +08:00
parent eaa37fe674
commit 36c87451d9
11 changed files with 186 additions and 35 deletions

View File

@ -208,9 +208,11 @@ export class WindowService {
const oauthProviderUrls = [
'https://account.siliconflow.cn/oauth',
'https://cloud.siliconflow.cn/bills',
'https://cloud.siliconflow.cn/expensebill',
'https://aihubmix.com/token',
'https://aihubmix.com/topup'
'https://aihubmix.com/topup',
'https://aihubmix.com/statistics'
]
if (oauthProviderUrls.some((link) => url.startsWith(link))) {

View File

@ -11,6 +11,7 @@ interface Props extends ButtonProps {
const OAuthButton: FC<Props> = ({ provider, onSuccess, ...buttonProps }) => {
const { t } = useTranslation()
const onAuth = () => {
const handleSuccess = (key: string) => {
if (key.trim()) {
@ -29,8 +30,8 @@ const OAuthButton: FC<Props> = ({ provider, onSuccess, ...buttonProps }) => {
}
return (
<Button onClick={onAuth} {...buttonProps}>
{t('auth.get_key')}
<Button type="primary" onClick={onAuth} shape="round" {...buttonProps}>
{t('settings.provider.oauth.button', { provider: t(`provider.${provider.id}`) })}
</Button>
)
}

View File

@ -148,7 +148,7 @@ export const PROVIDER_CONFIG = {
url: 'https://api.siliconflow.cn'
},
websites: {
official: 'https://www.siliconflow.cn/',
official: 'https://www.siliconflow.cn',
apiKey: 'https://cloud.siliconflow.cn/i/d1nTBKXU',
docs: 'https://docs.siliconflow.cn/',
models: 'https://docs.siliconflow.cn/docs/model-names'

View File

@ -1257,10 +1257,16 @@
"basic_auth.user_name.tip": "Left empty to disable",
"basic_auth.password": "Password",
"basic_auth.password.tip": "",
"charge": "Charge",
"charge": "Balance Recharge",
"bills": "Fee Bills",
"check": "Check",
"check_all_keys": "Check All Keys",
"check_multiple_keys": "Check Multiple API Keys",
"oauth": {
"button": "Login with {{provider}}",
"description": "This service is provided by <website>{{provider}}</website>",
"official_website": "Official Website"
},
"copilot": {
"auth_failed": "Github Copilot authentication failed.",
"auth_success": "GitHub Copilot authentication successful.",

View File

@ -1256,10 +1256,16 @@
"basic_auth.user_name.tip": "空欄で無効化",
"basic_auth.password": "パスワード",
"basic_auth.password.tip": "",
"charge": "充電",
"charge": "残高充電",
"bills": "費用帳單",
"check": "チェック",
"check_all_keys": "すべてのキーをチェック",
"check_multiple_keys": "複数のAPIキーをチェック",
"oauth": {
"button": "{{provider}} アカウントでログイン",
"description": "本サービスは<website>{{provider}}</website>によって提供されます",
"official_website": "公式サイト"
},
"copilot": {
"auth_failed": "Github Copilotの認証に失敗しました。",
"auth_success": "Github Copilotの認証が成功しました",

View File

@ -1256,10 +1256,16 @@
"basic_auth.user_name.tip": "Оставить пустым для отключения",
"basic_auth.password": "Пароль",
"basic_auth.password.tip": "",
"charge": "Пополнить",
"charge": "Пополнить баланс",
"bills": "Счета за услуги",
"check": "Проверить",
"check_all_keys": "Проверить все ключи",
"check_multiple_keys": "Проверить несколько ключей API",
"oauth": {
"button": "Войти с {{provider}}",
"description": "Сервис предоставляется <website>{{provider}}</website>",
"official_website": "Официальный сайт"
},
"copilot": {
"auth_failed": "Github Copilot认证失败",
"auth_success": "Github Copilot认证成功",

View File

@ -837,7 +837,7 @@
},
"joplin": {
"check": {
"button": "检",
"button": "检",
"empty_token": "请先输入 Joplin 授权令牌",
"empty_url": "请先输入 Joplin 剪裁服务监听 URL",
"fail": "Joplin 连接验证失败",
@ -866,7 +866,7 @@
"notion.auto_split": "导出对话时自动分页",
"notion.auto_split_tip": "当要导出的话题过长时自动分页导出到Notion",
"notion.check": {
"button": "检",
"button": "检",
"empty_api_key": "未配置 API key",
"empty_database_id": "未配置 Database ID",
"error": "连接异常,请检查网络及 API key 和 Database ID 是否正确",
@ -935,7 +935,7 @@
},
"yuque": {
"check": {
"button": "检",
"button": "检",
"empty_repo_url": "请先输入知识库URL",
"empty_token": "请先输入语雀Token",
"fail": "语雀连接验证失败",
@ -969,8 +969,8 @@
"root_path": "文档根路径",
"root_path_placeholder": "例如:/CherryStudio",
"check": {
"title": "连接检",
"button": "检",
"title": "连接检",
"button": "检",
"empty_config": "请填写API地址和令牌",
"success": "连接成功",
"fail": "连接失败请检查API地址和令牌",
@ -1206,20 +1206,20 @@
"models.add.model_name": "模型名称",
"models.add.model_name.placeholder": "例如 GPT-3.5",
"models.check.all": "所有",
"models.check.all_models_passed": "所有模型检通过",
"models.check.button_caption": "健康检",
"models.check.all_models_passed": "所有模型检通过",
"models.check.button_caption": "健康检",
"models.check.disabled": "关闭",
"models.check.enable_concurrent": "并发检",
"models.check.enable_concurrent": "并发检",
"models.check.enabled": "开启",
"models.check.failed": "失败",
"models.check.keys_status_count": "通过:{{count_passed}}个密钥,失败:{{count_failed}}个密钥",
"models.check.model_status_summary": "{{provider}}: {{count_passed}} 个模型完成健康检(其中 {{count_partial}} 个模型用某些密钥无法访问),{{count_failed}} 个模型完全无法访问。",
"models.check.model_status_summary": "{{provider}}: {{count_passed}} 个模型完成健康检(其中 {{count_partial}} 个模型用某些密钥无法访问),{{count_failed}} 个模型完全无法访问。",
"models.check.no_api_keys": "未找到API密钥请先添加API密钥。",
"models.check.passed": "通过",
"models.check.select_api_key": "选择要使用的API密钥",
"models.check.single": "单个",
"models.check.start": "开始",
"models.check.title": "模型健康检",
"models.check.title": "模型健康检",
"models.check.use_all_keys": "使用密钥",
"models.default_assistant_model": "默认助手模型",
"models.default_assistant_model_description": "创建新助手时使用的模型,如果助手未设置模型,则使用此模型",
@ -1257,10 +1257,16 @@
"basic_auth.user_name.tip": "留空以禁用",
"basic_auth.password": "密码",
"basic_auth.password.tip": "",
"charge": "充值",
"check": "检查",
"check_all_keys": "检查所有密钥",
"check_multiple_keys": "检查多个 API 密钥",
"charge": "余额充值",
"bills": "费用账单",
"check": "检测",
"check_all_keys": "检测所有密钥",
"check_multiple_keys": "检测多个 API 密钥",
"oauth": {
"button": "使用{{provider}}账号登录",
"description": "本服务由<website>{{provider}}</website>提供",
"official_website": "官方网站"
},
"copilot": {
"auth_failed": "Github Copilot 认证失败",
"auth_success": "Github Copilot 认证成功",
@ -1291,8 +1297,8 @@
"docs_more_details": "获取更多详情",
"get_api_key": "点击这里获取密钥",
"is_not_support_array_content": "开启兼容模式",
"no_models_for_check": "没有可以被检的模型(例如对话模型)",
"not_checked": "未检",
"no_models_for_check": "没有可以被检的模型(例如对话模型)",
"not_checked": "未检",
"remove_duplicate_keys": "移除重复密钥",
"remove_invalid_keys": "删除无效密钥",
"search": "搜索模型平台...",
@ -1358,13 +1364,13 @@
"blacklist": "黑名单",
"blacklist_description": "在搜索结果中不会出现以下网站的结果",
"blacklist_tooltip": "请使用以下格式(换行分隔)\n匹配模式: *://*.example.com/*\n正则表达式: /example\\.(net|org)/",
"check": "检",
"check": "检",
"check_failed": "验证失败",
"check_success": "验证成功",
"overwrite": "覆盖服务商搜索",
"overwrite_tooltip": "强制使用搜索服务商而不是大语言模型进行搜索",
"get_api_key": "点击这里获取密钥",
"no_provider_selected": "请选择搜索服务商后再检",
"no_provider_selected": "请选择搜索服务商后再检",
"search_max_result": "搜索结果个数",
"search_provider": "搜索服务商",
"search_provider_placeholder": "选择一个搜索服务商",

View File

@ -1256,10 +1256,16 @@
"basic_auth.user_name.tip": "留空以停用",
"basic_auth.password": "密碼",
"basic_auth.password.tip": "",
"charge": "儲值",
"charge": "餘額充值",
"bills": "費用帳單",
"check": "檢查",
"check_all_keys": "檢查所有金鑰",
"check_multiple_keys": "檢查多個 API 金鑰",
"oauth": {
"button": "使用{{provider}}帳號登入",
"description": "本服務由<website>{{provider}}</website>提供",
"official_website": "官方網站"
},
"copilot": {
"auth_failed": "Github Copilot認證失敗",
"auth_success": "Github Copilot 認證成功",

View File

@ -0,0 +1,92 @@
import AiHubMixProviderLogo from '@renderer/assets/images/providers/aihubmix.webp'
import SiliconFlowProviderLogo from '@renderer/assets/images/providers/silicon.png'
import { HStack } from '@renderer/components/Layout'
import OAuthButton from '@renderer/components/OAuth/OAuthButton'
import { PROVIDER_CONFIG } from '@renderer/config/providers'
import { Provider } from '@renderer/types'
import { providerBills, providerCharge } from '@renderer/utils/oauth'
import { Button } from 'antd'
import { isEmpty } from 'lodash'
import { ReceiptText } from 'lucide-react'
import { CircleDollarSign } from 'lucide-react'
import { FC } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import styled from 'styled-components'
interface Props {
provider: Provider
setApiKey: (apiKey: string) => void
}
const PROVIDER_LOGO_MAP = {
silicon: SiliconFlowProviderLogo,
aihubmix: AiHubMixProviderLogo
}
const ProviderOAuth: FC<Props> = ({ provider, setApiKey }) => {
const { t } = useTranslation()
const providerWebsite =
PROVIDER_CONFIG[provider.id]?.api?.url.replace('https://', '').replace('api.', '') || provider.name
return (
<Container>
<ProviderLogo src={PROVIDER_LOGO_MAP[provider.id]} />
{isEmpty(provider.apiKey) ? (
<OAuthButton provider={provider} onSuccess={setApiKey}>
{t('settings.provider.oauth.button', { provider: t(`provider.${provider.id}`) })}
</OAuthButton>
) : (
<HStack gap={10}>
<Button shape="round" icon={<CircleDollarSign size={16} />} onClick={() => providerCharge(provider.id)}>
{t('settings.provider.charge')}
</Button>
<Button shape="round" icon={<ReceiptText size={16} />} onClick={() => providerBills(provider.id)}>
{t('settings.provider.bills')}
</Button>
</HStack>
)}
<Description>
<Trans
i18nKey="settings.provider.oauth.description"
components={{
website: (
<OfficialWebsite href={PROVIDER_CONFIG[provider.id].websites.official} target="_blank" rel="noreferrer" />
)
}}
values={{ provider: providerWebsite }}
/>
</Description>
</Container>
)
}
const Container = styled.div`
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 15px;
padding: 20px;
`
const ProviderLogo = styled.img`
width: 60px;
height: 60px;
border-radius: 50%;
`
const Description = styled.div`
font-size: 12px;
color: var(--color-text-2);
display: flex;
align-items: center;
gap: 5px;
`
const OfficialWebsite = styled.a`
text-decoration: none;
color: var(--color-text-2);
`
export default ProviderOAuth

View File

@ -1,7 +1,6 @@
import { CheckOutlined, LoadingOutlined } from '@ant-design/icons'
import { StreamlineGoodHealthAndWellBeing } from '@renderer/components/Icons/SVGIcon'
import { HStack } from '@renderer/components/Layout'
import OAuthButton from '@renderer/components/OAuth/OAuthButton'
import { isEmbeddingModel, isRerankModel } from '@renderer/config/models'
import { PROVIDER_CONFIG } from '@renderer/config/providers'
import { useTheme } from '@renderer/context/ThemeProvider'
@ -10,10 +9,9 @@ import i18n from '@renderer/i18n'
import { isOpenAIProvider } from '@renderer/providers/AiProvider/ProviderFactory'
import { checkApi, formatApiKeys } from '@renderer/services/ApiService'
import { checkModelsHealth, ModelCheckStatus } from '@renderer/services/HealthCheckService'
import { isProviderSupportAuth, isProviderSupportCharge } from '@renderer/services/ProviderService'
import { isProviderSupportAuth } from '@renderer/services/ProviderService'
import { Provider } from '@renderer/types'
import { formatApiHost } from '@renderer/utils/api'
import { providerCharge } from '@renderer/utils/oauth'
import { Button, Divider, Flex, Input, Space, Switch, Tooltip } from 'antd'
import Link from 'antd/es/typography/Link'
import { debounce, isEmpty } from 'lodash'
@ -38,6 +36,7 @@ import LMStudioSettings from './LMStudioSettings'
import ModelList, { ModelStatus } from './ModelList'
import ModelListSearchBar from './ModelListSearchBar'
import OllamSettings from './OllamaSettings'
import ProviderOAuth from './ProviderOAuth'
import ProviderSettingsPopup from './ProviderSettingsPopup'
import SelectProviderModelPopup from './SelectProviderModelPopup'
@ -323,6 +322,16 @@ const ProviderSetting: FC<Props> = ({ provider: _provider }) => {
/>
</SettingTitle>
<Divider style={{ width: '100%', margin: '10px 0' }} />
{isProviderSupportAuth(provider) && (
<ProviderOAuth
provider={provider}
setApiKey={(v) => {
setApiKey(v)
setInputValue(v)
updateProvider({ ...provider, apiKey: v })
}}
/>
)}
<SettingSubtitle style={{ marginTop: 5 }}>{t('settings.provider.api_key')}</SettingSubtitle>
<Space.Compact style={{ width: '100%', marginTop: 5 }}>
<Input.Password
@ -342,7 +351,6 @@ const ProviderSetting: FC<Props> = ({ provider: _provider }) => {
autoFocus={provider.enabled && apiKey === ''}
disabled={provider.id === 'copilot'}
/>
{isProviderSupportAuth(provider) && <OAuthButton provider={provider} onSuccess={setApiKey} />}
<Button
type={apiValid ? 'primary' : 'default'}
ghost={apiValid}
@ -357,11 +365,6 @@ const ProviderSetting: FC<Props> = ({ provider: _provider }) => {
<SettingHelpLink target="_blank" href={apiKeyWebsite}>
{t('settings.provider.get_api_key')}
</SettingHelpLink>
{isProviderSupportCharge(provider) && (
<SettingHelpLink onClick={() => providerCharge(provider.id)}>
{t('settings.provider.charge')}
</SettingHelpLink>
)}
</HStack>
<SettingHelpText>{t('settings.provider.api_key.tip')}</SettingHelpText>
</SettingHelpTextRow>

View File

@ -80,3 +80,26 @@ export const providerCharge = async (provider: string) => {
`width=${width},height=${height},toolbar=no,location=no,status=no,menubar=no,scrollbars=yes,resizable=yes,alwaysOnTop=yes,alwaysRaised=yes`
)
}
export const providerBills = async (provider: string) => {
const billsUrlMap = {
silicon: {
url: 'https://cloud.siliconflow.cn/bills',
width: 900,
height: 700
},
aihubmix: {
url: `https://aihubmix.com/statistics?client_id=cherry_studio_oauth&lang=${getLanguageCode()}&aff=SJyh`,
width: 900,
height: 700
}
}
const { url, width, height } = billsUrlMap[provider]
window.open(
url,
'oauth',
`width=${width},height=${height},toolbar=no,location=no,status=no,menubar=no,scrollbars=yes,resizable=yes,alwaysOnTop=yes,alwaysRaised=yes`
)
}