mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-05 20:41:30 +08:00
feat: support codex.
This commit is contained in:
parent
a619000340
commit
3abc948b77
@ -334,6 +334,18 @@ export enum IpcChannel {
|
||||
Anthropic_HasCredentials = 'anthropic:has-credentials',
|
||||
Anthropic_ClearCredentials = 'anthropic:clear-credentials',
|
||||
|
||||
// OpenAI OAuth
|
||||
OpenAI_StartOAuthFlow = 'openai:start-oauth-flow',
|
||||
OpenAI_CompleteOAuthWithRedirectUrl = 'openai:complete-oauth-with-redirect-url',
|
||||
OpenAI_CancelOAuthFlow = 'openai:cancel-oauth-flow',
|
||||
OpenAI_GetAccessToken = 'openai:get-access-token',
|
||||
OpenAI_GetApiKey = 'openai:get-api-key',
|
||||
OpenAI_GetIdToken = 'openai:get-id-token',
|
||||
OpenAI_GetAccountId = 'openai:get-account-id',
|
||||
OpenAI_GetSessionId = 'openai:get-session-id',
|
||||
OpenAI_HasCredentials = 'openai:has-credentials',
|
||||
OpenAI_ClearCredentials = 'openai:clear-credentials',
|
||||
|
||||
// CodeTools
|
||||
CodeTools_Run = 'code-tools:run',
|
||||
CodeTools_GetAvailableTerminals = 'code-tools:get-available-terminals',
|
||||
|
||||
@ -6,6 +6,7 @@ import { loggerService } from '@logger'
|
||||
import { isLinux, isMac, isPortable, isWin } from '@main/constant'
|
||||
import { generateSignature } from '@main/integration/cherryai'
|
||||
import anthropicService from '@main/services/AnthropicService'
|
||||
import openAIService from '@main/services/OpenAIService'
|
||||
import { getBinaryPath, isBinaryExists, runInstallScript } from '@main/utils/process'
|
||||
import { handleZoomFactor } from '@main/utils/zoom'
|
||||
import type { SpanEntity, TokenUsage } from '@mcp-trace/trace-core'
|
||||
@ -885,6 +886,20 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
||||
ipcMain.handle(IpcChannel.Anthropic_HasCredentials, () => anthropicService.hasCredentials())
|
||||
ipcMain.handle(IpcChannel.Anthropic_ClearCredentials, () => anthropicService.clearCredentials())
|
||||
|
||||
// OpenAI OAuth
|
||||
ipcMain.handle(IpcChannel.OpenAI_StartOAuthFlow, () => openAIService.startOAuthFlow())
|
||||
ipcMain.handle(IpcChannel.OpenAI_CompleteOAuthWithRedirectUrl, (_, url: string) =>
|
||||
openAIService.completeOAuthWithRedirectUrl(url)
|
||||
)
|
||||
ipcMain.handle(IpcChannel.OpenAI_CancelOAuthFlow, () => openAIService.cancelOAuthFlow())
|
||||
ipcMain.handle(IpcChannel.OpenAI_GetAccessToken, () => openAIService.getValidAccessToken())
|
||||
ipcMain.handle(IpcChannel.OpenAI_GetApiKey, () => openAIService.getApiKey())
|
||||
ipcMain.handle(IpcChannel.OpenAI_GetIdToken, () => openAIService.getIdToken())
|
||||
ipcMain.handle(IpcChannel.OpenAI_GetAccountId, () => openAIService.getAccountId())
|
||||
ipcMain.handle(IpcChannel.OpenAI_GetSessionId, () => openAIService.getSessionId())
|
||||
ipcMain.handle(IpcChannel.OpenAI_HasCredentials, () => openAIService.hasCredentials())
|
||||
ipcMain.handle(IpcChannel.OpenAI_ClearCredentials, () => openAIService.clearCredentials())
|
||||
|
||||
// CodeTools
|
||||
ipcMain.handle(IpcChannel.CodeTools_Run, codeToolsService.run)
|
||||
ipcMain.handle(IpcChannel.CodeTools_GetAvailableTerminals, () => codeToolsService.getAvailableTerminalsForPlatform())
|
||||
|
||||
266
src/main/services/OpenAIService.ts
Normal file
266
src/main/services/OpenAIService.ts
Normal file
@ -0,0 +1,266 @@
|
||||
import path from 'node:path'
|
||||
|
||||
import { loggerService } from '@logger'
|
||||
import { getConfigDir } from '@main/utils/file'
|
||||
import * as crypto from 'crypto'
|
||||
import { net, shell } from 'electron'
|
||||
import { promises } from 'fs'
|
||||
import { dirname } from 'path'
|
||||
|
||||
const logger = loggerService.withContext('OpenAIOAuth')
|
||||
|
||||
// Client configuration
|
||||
const DEFAULT_CLIENT_ID = 'app_EMoamEEZ73f0CkXaXp7hrann';
|
||||
const CREDS_PATH = path.join(getConfigDir(), 'oauth', 'openai.json')
|
||||
const REDIRECT_URI = 'http://localhost:1455/auth/callback'
|
||||
const ISSUER = 'https://auth.openai.com'
|
||||
|
||||
interface Credentials {
|
||||
access_token: string
|
||||
refresh_token: string
|
||||
expires_at: number
|
||||
id_token?: string
|
||||
}
|
||||
|
||||
interface PKCEState {
|
||||
verifier: string
|
||||
challenge: string
|
||||
state: string
|
||||
}
|
||||
|
||||
class OpenAIService {
|
||||
private current: PKCEState | null = null
|
||||
|
||||
private generatePKCEState(): PKCEState {
|
||||
const verifier = crypto.randomBytes(32).toString('base64url')
|
||||
const challenge = crypto.createHash('sha256').update(verifier).digest('base64url')
|
||||
const state = crypto.randomBytes(16).toString('base64url')
|
||||
return { verifier, challenge, state }
|
||||
}
|
||||
|
||||
private buildAuthorizeUrl(pkce: PKCEState, clientId: string): string {
|
||||
const url = new URL(`${ISSUER}/oauth/authorize`)
|
||||
url.searchParams.set('response_type', 'code')
|
||||
url.searchParams.set('client_id', clientId)
|
||||
url.searchParams.set('redirect_uri', REDIRECT_URI)
|
||||
url.searchParams.set('scope', 'openid profile email offline_access')
|
||||
url.searchParams.set('code_challenge', pkce.challenge)
|
||||
url.searchParams.set('code_challenge_method', 'S256')
|
||||
url.searchParams.set('state', pkce.state)
|
||||
// Only required OAuth params; remove non-essential extras
|
||||
logger.debug(`Built OpenAI authorize URL: ${url.toString()}`)
|
||||
return url.toString()
|
||||
}
|
||||
|
||||
private async exchangeCodeForTokens(
|
||||
code: string,
|
||||
verifier: string,
|
||||
clientId: string
|
||||
): Promise<Credentials> {
|
||||
const response = await net.fetch(`${ISSUER}/oauth/token`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: new URLSearchParams({
|
||||
grant_type: 'authorization_code',
|
||||
code,
|
||||
redirect_uri: REDIRECT_URI,
|
||||
client_id: clientId,
|
||||
code_verifier: verifier
|
||||
}).toString()
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`OpenAI token exchange failed: ${response.status} ${response.statusText}`)
|
||||
}
|
||||
const data = await response.json()
|
||||
return {
|
||||
access_token: data.access_token,
|
||||
refresh_token: data.refresh_token,
|
||||
expires_at: Date.now() + data.expires_in * 1000,
|
||||
id_token: data.id_token
|
||||
}
|
||||
}
|
||||
|
||||
private async refreshAccessToken(refreshToken: string, clientId: string): Promise<Credentials> {
|
||||
const response = await net.fetch(`${ISSUER}/oauth/token`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: new URLSearchParams({
|
||||
grant_type: 'refresh_token',
|
||||
refresh_token: refreshToken,
|
||||
client_id: clientId
|
||||
}).toString()
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`OpenAI token refresh failed: ${response.status} ${response.statusText}`)
|
||||
}
|
||||
const data = await response.json()
|
||||
return {
|
||||
access_token: data.access_token,
|
||||
refresh_token: data.refresh_token ?? refreshToken,
|
||||
expires_at: Date.now() + data.expires_in * 1000,
|
||||
id_token: data.id_token
|
||||
}
|
||||
}
|
||||
|
||||
private async saveCredentials(creds: Credentials) {
|
||||
await promises.mkdir(dirname(CREDS_PATH), { recursive: true })
|
||||
await promises.writeFile(CREDS_PATH, JSON.stringify(creds, null, 2))
|
||||
await promises.chmod(CREDS_PATH, 0o600)
|
||||
}
|
||||
|
||||
private async loadCredentials(): Promise<Credentials | null> {
|
||||
try {
|
||||
const txt = await promises.readFile(CREDS_PATH, 'utf-8')
|
||||
return JSON.parse(txt)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
public async getValidAccessToken(): Promise<string | null> {
|
||||
const clientId = DEFAULT_CLIENT_ID
|
||||
if (!clientId || clientId.startsWith('0000')) {
|
||||
logger.warn('OPENAI_OAUTH_CLIENT_ID is not set. OAuth may fail until configured.')
|
||||
}
|
||||
const creds = await this.loadCredentials()
|
||||
if (!creds) return null
|
||||
if (creds.expires_at > Date.now() + 60000) {
|
||||
return creds.access_token
|
||||
}
|
||||
try {
|
||||
const refreshed = await this.refreshAccessToken(creds.refresh_token, clientId)
|
||||
// Preserve previous id_token if refresh did not include one
|
||||
const merged: Credentials = { ...refreshed, id_token: refreshed.id_token ?? creds.id_token }
|
||||
await this.saveCredentials(merged)
|
||||
return merged.access_token
|
||||
} catch (e) {
|
||||
logger.error('OpenAI access token refresh failed', e as Error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
public async getApiKey(): Promise<string | null> {
|
||||
// For OAuth-based access, the access token serves as bearer token
|
||||
return this.getValidAccessToken()
|
||||
}
|
||||
|
||||
public async startOAuthFlow(): Promise<string> {
|
||||
const clientId = DEFAULT_CLIENT_ID
|
||||
if (!clientId || clientId.startsWith('0000')) {
|
||||
logger.warn('OPENAI_OAUTH_CLIENT_ID is not set. Please configure it for production use.')
|
||||
}
|
||||
// If already have valid access, short-circuit
|
||||
const existing = await this.getValidAccessToken()
|
||||
if (existing) return 'already_authenticated'
|
||||
|
||||
this.current = this.generatePKCEState()
|
||||
const authUrl = this.buildAuthorizeUrl(this.current, clientId)
|
||||
await shell.openExternal(authUrl)
|
||||
return authUrl
|
||||
}
|
||||
|
||||
public async completeOAuthWithRedirectUrl(redirectUrl: string): Promise<string> {
|
||||
if (!this.current) {
|
||||
throw new Error('OAuth flow not started. Please call startOAuthFlow first.')
|
||||
}
|
||||
const clientId = DEFAULT_CLIENT_ID
|
||||
const url = new URL(redirectUrl)
|
||||
const code = url.searchParams.get('code') || ''
|
||||
const state = url.searchParams.get('state') || ''
|
||||
if (!code) {
|
||||
throw new Error('Authorization code not found in redirect URL')
|
||||
}
|
||||
if (!state || state !== this.current.state) {
|
||||
throw new Error('State mismatch detected')
|
||||
}
|
||||
try {
|
||||
const base = await this.exchangeCodeForTokens(code, this.current.verifier, clientId)
|
||||
await this.saveCredentials(base)
|
||||
this.current = null
|
||||
return base.access_token
|
||||
} catch (e) {
|
||||
this.current = null
|
||||
logger.error('OpenAI OAuth code exchange failed', e as Error)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
public cancelOAuthFlow(): void {
|
||||
if (this.current) {
|
||||
logger.info('Cancelling OpenAI OAuth flow')
|
||||
this.current = null
|
||||
}
|
||||
}
|
||||
|
||||
public async clearCredentials(): Promise<void> {
|
||||
try {
|
||||
await promises.unlink(CREDS_PATH)
|
||||
logger.info('OpenAI credentials cleared')
|
||||
} catch (e) {
|
||||
if ((e as NodeJS.ErrnoException).code !== 'ENOENT') throw e
|
||||
}
|
||||
}
|
||||
|
||||
public async hasCredentials(): Promise<boolean> {
|
||||
const creds = await this.loadCredentials()
|
||||
return creds !== null
|
||||
}
|
||||
|
||||
public async getIdToken(): Promise<string | null> {
|
||||
const creds = await this.loadCredentials()
|
||||
return creds?.id_token ?? null
|
||||
}
|
||||
|
||||
public async getAccountId(): Promise<string | null> {
|
||||
const idToken = await this.getIdToken()
|
||||
if (!idToken) return null
|
||||
try {
|
||||
const payload = this.decodeJwtPayload(idToken)
|
||||
if (!payload) return null
|
||||
// Try common fields for account/user identifiers
|
||||
const candidates = [
|
||||
payload.account_id,
|
||||
payload.chatgpt_user_id,
|
||||
payload.aid,
|
||||
payload.sub
|
||||
]
|
||||
const id = candidates.find((v) => typeof v === 'string' && v.length > 0)
|
||||
return id ?? null
|
||||
} catch (e) {
|
||||
logger.warn('Failed to parse OpenAI ID token for account id', e as Error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
public async getSessionId(): Promise<string | null> {
|
||||
// Derive a stable session id from ID token claims when possible
|
||||
const idToken = await this.getIdToken()
|
||||
if (!idToken) return null
|
||||
try {
|
||||
const payload = this.decodeJwtPayload(idToken)
|
||||
// Prefer standard-ish fields if present
|
||||
const rawCandidate =
|
||||
(payload && (payload.sid || payload.session_id || payload.jti || payload.sub)) || idToken
|
||||
const hash = crypto.createHash('sha256').update(String(rawCandidate)).digest('hex').slice(0, 32)
|
||||
return `sess_${hash}`
|
||||
} catch (e) {
|
||||
logger.warn('Failed to derive OpenAI session id', e as Error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
private decodeJwtPayload(token: string): any | null {
|
||||
const parts = token.split('.')
|
||||
if (parts.length < 2) return null
|
||||
const payload = parts[1]
|
||||
const normalized = payload.replace(/-/g, '+').replace(/_/g, '/')
|
||||
const padLen = (4 - (normalized.length % 4)) % 4
|
||||
const padded = normalized + '='.repeat(padLen)
|
||||
const json = Buffer.from(padded, 'base64').toString('utf8')
|
||||
return JSON.parse(json)
|
||||
}
|
||||
}
|
||||
|
||||
export default new OpenAIService()
|
||||
@ -481,6 +481,19 @@ const api = {
|
||||
hasCredentials: () => ipcRenderer.invoke(IpcChannel.Anthropic_HasCredentials),
|
||||
clearCredentials: () => ipcRenderer.invoke(IpcChannel.Anthropic_ClearCredentials)
|
||||
},
|
||||
openai_oauth: {
|
||||
startOAuthFlow: () => ipcRenderer.invoke(IpcChannel.OpenAI_StartOAuthFlow),
|
||||
completeOAuthWithRedirectUrl: (url: string) =>
|
||||
ipcRenderer.invoke(IpcChannel.OpenAI_CompleteOAuthWithRedirectUrl, url),
|
||||
cancelOAuthFlow: () => ipcRenderer.invoke(IpcChannel.OpenAI_CancelOAuthFlow),
|
||||
getAccessToken: () => ipcRenderer.invoke(IpcChannel.OpenAI_GetAccessToken),
|
||||
getApiKey: () => ipcRenderer.invoke(IpcChannel.OpenAI_GetApiKey),
|
||||
getIdToken: () => ipcRenderer.invoke(IpcChannel.OpenAI_GetIdToken),
|
||||
getAccountId: () => ipcRenderer.invoke(IpcChannel.OpenAI_GetAccountId),
|
||||
getSessionId: () => ipcRenderer.invoke(IpcChannel.OpenAI_GetSessionId),
|
||||
hasCredentials: () => ipcRenderer.invoke(IpcChannel.OpenAI_HasCredentials),
|
||||
clearCredentials: () => ipcRenderer.invoke(IpcChannel.OpenAI_ClearCredentials)
|
||||
},
|
||||
codeTools: {
|
||||
run: (
|
||||
cliTool: string,
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -4391,7 +4391,25 @@
|
||||
"official_website": "Official Website"
|
||||
},
|
||||
"openai": {
|
||||
"alert": "OpenAI Provider no longer support the old calling methods. If using a third-party API, please create a new service provider."
|
||||
"alert": "OpenAI provider no longer supports the old calling methods. If you are using a third-party API, please create a new service provider.",
|
||||
"auth_method": "Authentication Method",
|
||||
"oauth": "Web OAuth",
|
||||
"description": "OAuth Authentication",
|
||||
"description_detail": "Click 'Start Authorization' to open the OpenAI login page in your browser. After logging in, copy the entire URL that redirects to the local address (http://127.0.0.1:1455/auth/callback?...) and paste it here to complete authentication.",
|
||||
"start_auth": "Start Authorization",
|
||||
"authenticating": "Authenticating",
|
||||
"enter_redirect_url": "Redirect URL",
|
||||
"submit_url": "Submit URL",
|
||||
"url_placeholder": "Paste the full URL starting with http://127.0.0.1:1455/auth/callback",
|
||||
"cancel": "Cancel",
|
||||
"auth_success": "OpenAI OAuth authentication successful",
|
||||
"auth_failed": "OpenAI OAuth authentication failed",
|
||||
"url_error": "URL parsing or authentication failed, please try again",
|
||||
"authenticated": "Authenticated",
|
||||
"logout": "Logout",
|
||||
"logout_success": "Logged out",
|
||||
"logout_failed": "Logout failed",
|
||||
"apikey":"API key"
|
||||
},
|
||||
"remove_duplicate_keys": "Remove Duplicate Keys",
|
||||
"remove_invalid_keys": "Remove Invalid Keys",
|
||||
|
||||
@ -4391,7 +4391,25 @@
|
||||
"official_website": "官方网站"
|
||||
},
|
||||
"openai": {
|
||||
"alert": "OpenAI 服务商不再支持旧的调用方式,如果使用第三方 API 请新建服务商"
|
||||
"alert": "OpenAI 服务商不再支持旧的调用方式,如果使用第三方 API 请新建服务商",
|
||||
"auth_method": "认证方式",
|
||||
"oauth": "网页 OAuth",
|
||||
"description": "使用 OpenAI OAuth 登录",
|
||||
"description_detail": "点击开始授权将在浏览器中打开 OpenAI 登录页。登录后复制跳转到本地地址的整条 URL(http://127.0.0.1:1455/auth/callback?...),粘贴回此处完成认证。",
|
||||
"start_auth": "开始授权",
|
||||
"authenticating": "正在认证",
|
||||
"enter_redirect_url": "重定向 URL",
|
||||
"submit_url": "提交 URL",
|
||||
"url_placeholder": "粘贴以 http://127.0.0.1:1455/auth/callback 开头的完整 URL",
|
||||
"cancel": "取消",
|
||||
"auth_success": "OpenAI OAuth 认证成功",
|
||||
"auth_failed": "OpenAI OAuth 认证失败",
|
||||
"url_error": "URL 解析或认证失败,请重试",
|
||||
"authenticated": "已认证",
|
||||
"logout": "登出",
|
||||
"logout_success": "已登出",
|
||||
"logout_failed": "登出失败",
|
||||
"apikey":"API key"
|
||||
},
|
||||
"remove_duplicate_keys": "移除重复密钥",
|
||||
"remove_invalid_keys": "删除无效密钥",
|
||||
|
||||
@ -4391,7 +4391,25 @@
|
||||
"official_website": "官方網站"
|
||||
},
|
||||
"openai": {
|
||||
"alert": "OpenAI Provider 不再支援舊的呼叫方法。如果使用第三方 API,請建立新的服務供應商"
|
||||
"alert": "OpenAI Provider 不再支援舊的呼叫方法。如果使用第三方 API,請建立新的服務供應商",
|
||||
"auth_method": "認證方式",
|
||||
"oauth": "網頁 OAuth",
|
||||
"description": "使用 OpenAI OAuth 登入",
|
||||
"description_detail": "點擊開始授權後會在瀏覽器開啟 OpenAI 登入頁。登入後請複製跳轉至本機位址的完整 URL(http://127.0.0.1:1455/auth/callback?...),貼回此處完成認證。",
|
||||
"start_auth": "開始授權",
|
||||
"authenticating": "正在認證",
|
||||
"enter_redirect_url": "重新導向 URL",
|
||||
"submit_url": "提交 URL",
|
||||
"url_placeholder": "貼上以 http://127.0.0.1:1455/auth/callback 開頭的完整 URL",
|
||||
"cancel": "取消",
|
||||
"auth_success": "OpenAI OAuth 認證成功",
|
||||
"auth_failed": "OpenAI OAuth 認證失敗",
|
||||
"url_error": "URL 解析或認證失敗,請重試",
|
||||
"authenticated": "已認證",
|
||||
"logout": "登出",
|
||||
"logout_success": "已登出",
|
||||
"logout_failed": "登出失敗",
|
||||
"apikey":"API key"
|
||||
},
|
||||
"remove_duplicate_keys": "移除重複金鑰",
|
||||
"remove_invalid_keys": "刪除無效金鑰",
|
||||
|
||||
@ -0,0 +1,159 @@
|
||||
import { ExclamationCircleOutlined } from '@ant-design/icons'
|
||||
import { loggerService } from '@logger'
|
||||
import { Alert, Button, Input, Modal } from 'antd'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
const logger = loggerService.withContext('OpenAISettings')
|
||||
|
||||
enum AuthStatus {
|
||||
NOT_STARTED,
|
||||
AUTHENTICATING,
|
||||
AUTHENTICATED
|
||||
}
|
||||
|
||||
const OpenAISettings = () => {
|
||||
const { t } = useTranslation()
|
||||
const [authStatus, setAuthStatus] = useState<AuthStatus>(AuthStatus.NOT_STARTED)
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
const [urlModalVisible, setUrlModalVisible] = useState<boolean>(false)
|
||||
const [redirectUrl, setRedirectUrl] = useState<string>('')
|
||||
|
||||
useEffect(() => {
|
||||
const check = async () => {
|
||||
try {
|
||||
const hasCredentials = await window.api.openai_oauth.hasCredentials()
|
||||
if (hasCredentials) setAuthStatus(AuthStatus.AUTHENTICATED)
|
||||
} catch (e) {
|
||||
logger.error('Failed to check OpenAI OAuth state', e as Error)
|
||||
}
|
||||
}
|
||||
check()
|
||||
}, [])
|
||||
|
||||
const handleRedirectOAuth = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
await window.api.openai_oauth.startOAuthFlow()
|
||||
setAuthStatus(AuthStatus.AUTHENTICATING)
|
||||
setUrlModalVisible(true)
|
||||
} catch (e) {
|
||||
logger.error('OpenAI OAuth start failed', e as Error)
|
||||
window.toast.error(t('settings.provider.openai.auth_failed'))
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmitUrl = async () => {
|
||||
logger.info('Submitting OpenAI redirect URL')
|
||||
try {
|
||||
setLoading(true)
|
||||
await window.api.openai_oauth.completeOAuthWithRedirectUrl(redirectUrl)
|
||||
setAuthStatus(AuthStatus.AUTHENTICATED)
|
||||
setUrlModalVisible(false)
|
||||
window.toast.success(t('settings.provider.openai.auth_success'))
|
||||
} catch (e) {
|
||||
logger.error('OpenAI redirect URL submit failed', e as Error)
|
||||
window.toast.error(t('settings.provider.openai.url_error'))
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleCancelAuth = () => {
|
||||
window.api.openai_oauth.cancelOAuthFlow()
|
||||
setAuthStatus(AuthStatus.NOT_STARTED)
|
||||
setUrlModalVisible(false)
|
||||
setRedirectUrl('')
|
||||
}
|
||||
|
||||
const handleLogout = async () => {
|
||||
try {
|
||||
await window.api.openai_oauth.clearCredentials()
|
||||
setAuthStatus(AuthStatus.NOT_STARTED)
|
||||
window.toast.success(t('settings.provider.openai.logout_success'))
|
||||
} catch (e) {
|
||||
logger.error('OpenAI logout failed', e as Error)
|
||||
window.toast.error(t('settings.provider.openai.logout_failed'))
|
||||
}
|
||||
}
|
||||
|
||||
const renderContent = () => {
|
||||
switch (authStatus) {
|
||||
case AuthStatus.AUTHENTICATED:
|
||||
return (
|
||||
<StartContainer>
|
||||
<Alert
|
||||
type="success"
|
||||
message={t('settings.provider.openai.authenticated')}
|
||||
action={
|
||||
<Button type="primary" onClick={handleLogout}>
|
||||
{t('settings.provider.openai.logout')}
|
||||
</Button>
|
||||
}
|
||||
showIcon
|
||||
icon={<ExclamationCircleOutlined />}
|
||||
/>
|
||||
</StartContainer>
|
||||
)
|
||||
case AuthStatus.AUTHENTICATING:
|
||||
return (
|
||||
<StartContainer>
|
||||
<Alert
|
||||
type="info"
|
||||
message={t('settings.provider.openai.authenticating')}
|
||||
showIcon
|
||||
icon={<ExclamationCircleOutlined />}
|
||||
/>
|
||||
<Modal
|
||||
title={t('settings.provider.openai.enter_redirect_url')}
|
||||
open={urlModalVisible}
|
||||
onOk={handleSubmitUrl}
|
||||
onCancel={handleCancelAuth}
|
||||
okButtonProps={{ loading }}
|
||||
okText={t('settings.provider.openai.submit_url')}
|
||||
cancelText={t('settings.provider.openai.cancel')}
|
||||
centered>
|
||||
<Input
|
||||
value={redirectUrl}
|
||||
onChange={(e) => setRedirectUrl(e.target.value)}
|
||||
placeholder={t('settings.provider.openai.url_placeholder')}
|
||||
/>
|
||||
</Modal>
|
||||
</StartContainer>
|
||||
)
|
||||
default:
|
||||
return (
|
||||
<StartContainer>
|
||||
<Alert
|
||||
type="info"
|
||||
message={t('settings.provider.openai.description')}
|
||||
description={t('settings.provider.openai.description_detail')}
|
||||
action={
|
||||
<Button type="primary" loading={loading} onClick={handleRedirectOAuth}>
|
||||
{t('settings.provider.openai.start_auth')}
|
||||
</Button>
|
||||
}
|
||||
showIcon
|
||||
icon={<ExclamationCircleOutlined />}
|
||||
/>
|
||||
</StartContainer>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return <Container>{renderContent()}</Container>
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
padding-top: 10px;
|
||||
`
|
||||
|
||||
const StartContainer = styled.div`
|
||||
margin-bottom: 10px;
|
||||
`
|
||||
|
||||
export default OpenAISettings
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import OpenAIAlert from '@renderer/components/Alert/OpenAIAlert'
|
||||
import { LoadingIcon } from '@renderer/components/Icons'
|
||||
import { HStack } from '@renderer/components/Layout'
|
||||
import { ApiKeyListPopup } from '@renderer/components/Popups/ApiKeyListPopup'
|
||||
@ -62,6 +61,7 @@ import DMXAPISettings from './DMXAPISettings'
|
||||
import GithubCopilotSettings from './GithubCopilotSettings'
|
||||
import GPUStackSettings from './GPUStackSettings'
|
||||
import LMStudioSettings from './LMStudioSettings'
|
||||
import OpenAISettings from './OpenAISettings'
|
||||
import OVMSSettings from './OVMSSettings'
|
||||
import ProviderOAuth from './ProviderOAuth'
|
||||
import SelectProviderModelPopup from './SelectProviderModelPopup'
|
||||
@ -372,7 +372,7 @@ const ProviderSetting: FC<Props> = ({ providerId }) => {
|
||||
: t('settings.provider.api_host_tooltip')
|
||||
|
||||
const isAnthropicOAuth = () => provider.id === 'anthropic' && provider.authType === 'oauth'
|
||||
|
||||
const isOpenAIOAuth = () => provider.id === 'openai' && provider.authType === 'oauth'
|
||||
return (
|
||||
<SettingContainer theme={theme} style={{ background: 'var(--color-background)' }}>
|
||||
<SettingTitle>
|
||||
@ -407,7 +407,20 @@ const ProviderSetting: FC<Props> = ({ providerId }) => {
|
||||
</SettingTitle>
|
||||
<Divider style={{ width: '100%', margin: '10px 0' }} />
|
||||
{isProviderSupportAuth(provider) && <ProviderOAuth providerId={provider.id} />}
|
||||
{provider.id === 'openai' && <OpenAIAlert />}
|
||||
{provider.id === 'openai' && (
|
||||
<>
|
||||
<SettingSubtitle style={{ marginTop: 5 }}>{t('settings.provider.openai.auth_method')}</SettingSubtitle>
|
||||
<Select
|
||||
style={{ width: '40%', marginTop: 5, marginBottom: 10 }}
|
||||
value={provider.authType || 'apiKey'}
|
||||
onChange={(value) => updateProvider({ authType: value })}
|
||||
options={[
|
||||
{ value: 'apiKey', label: t('settings.provider.openai.apikey') },
|
||||
{ value: 'oauth', label: t('settings.provider.openai.oauth') }
|
||||
]}
|
||||
/>
|
||||
{provider.authType === 'oauth' && <OpenAISettings />}
|
||||
</>)}
|
||||
{provider.id === 'ovms' && <OVMSSettings />}
|
||||
{isDmxapi && <DMXAPISettings providerId={provider.id} />}
|
||||
{provider.id === 'anthropic' && (
|
||||
@ -425,7 +438,7 @@ const ProviderSetting: FC<Props> = ({ providerId }) => {
|
||||
{provider.authType === 'oauth' && <AnthropicSettings />}
|
||||
</>
|
||||
)}
|
||||
{!hideApiInput && !isAnthropicOAuth() && (
|
||||
{!hideApiInput && !isAnthropicOAuth() && !isOpenAIOAuth() && (
|
||||
<>
|
||||
{!hideApiKeyInput && (
|
||||
<>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user