mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-24 18:50:56 +08:00
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:
parent
eaa37fe674
commit
36c87451d9
@ -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))) {
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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.",
|
||||
|
||||
@ -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の認証が成功しました",
|
||||
|
||||
@ -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认证成功",
|
||||
|
||||
@ -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": "选择一个搜索服务商",
|
||||
|
||||
@ -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 認證成功",
|
||||
|
||||
@ -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
|
||||
@ -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>
|
||||
|
||||
@ -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`
|
||||
)
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user