mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-19 14:41:24 +08:00
Merge efa3ed8d29 into 8ab375161d
This commit is contained in:
commit
1e9a0598c2
@ -349,6 +349,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,8 @@ 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 {
|
||||
autoDiscoverGitBash,
|
||||
getBinaryPath,
|
||||
@ -968,6 +970,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())
|
||||
|
||||
256
src/main/services/OpenAIService.ts
Normal file
256
src/main/services/OpenAIService.ts
Normal file
@ -0,0 +1,256 @@
|
||||
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
|
||||
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 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 (error) {
|
||||
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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')
|
||||
try {
|
||||
return JSON.parse(json)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new OpenAIService()
|
||||
@ -518,6 +518,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
@ -4573,7 +4573,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.",
|
||||
"apikey": "API key",
|
||||
"auth_failed": "OpenAI OAuth authentication failed",
|
||||
"auth_method": "Authentication Method",
|
||||
"auth_success": "OpenAI OAuth authentication successful",
|
||||
"authenticated": "Authenticated",
|
||||
"authenticating": "Authenticating",
|
||||
"cancel": "Cancel",
|
||||
"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://localhost:1455/auth/callback?...) and paste it here to complete authentication.",
|
||||
"enter_redirect_url": "Redirect URL",
|
||||
"logout": "Logout",
|
||||
"logout_failed": "Logout failed",
|
||||
"logout_success": "Logged out",
|
||||
"oauth": "Web OAuth",
|
||||
"start_auth": "Start Authorization",
|
||||
"submit_url": "Submit URL",
|
||||
"url_error": "URL parsing or authentication failed, please try again",
|
||||
"url_placeholder": "Paste the full URL starting with http://localhost:1455/auth/callback"
|
||||
},
|
||||
"remove_duplicate_keys": "Remove Duplicate Keys",
|
||||
"remove_invalid_keys": "Remove Invalid Keys",
|
||||
|
||||
@ -4573,7 +4573,25 @@
|
||||
"official_website": "官方网站"
|
||||
},
|
||||
"openai": {
|
||||
"alert": "OpenAI 服务商不再支持旧的调用方式,如果使用第三方 API 请新建服务商"
|
||||
"alert": "OpenAI 服务商不再支持旧的调用方式,如果使用第三方 API 请新建服务商",
|
||||
"apikey": "API key",
|
||||
"auth_failed": "OpenAI OAuth 认证失败",
|
||||
"auth_method": "认证方式",
|
||||
"auth_success": "OpenAI OAuth 认证成功",
|
||||
"authenticated": "已认证",
|
||||
"authenticating": "正在认证",
|
||||
"cancel": "取消",
|
||||
"description": "使用 OpenAI OAuth 登录",
|
||||
"description_detail": "点击开始授权将在浏览器中打开 OpenAI 登录页。登录后复制跳转到本地地址的整条 URL(http://localhost:1455/auth/callback?...),粘贴回此处完成认证。",
|
||||
"enter_redirect_url": "重定向 URL",
|
||||
"logout": "登出",
|
||||
"logout_failed": "登出失败",
|
||||
"logout_success": "已登出",
|
||||
"oauth": "网页 OAuth",
|
||||
"start_auth": "开始授权",
|
||||
"submit_url": "提交 URL",
|
||||
"url_error": "URL 解析或认证失败,请重试",
|
||||
"url_placeholder": "粘贴以 http://localhost:1455/auth/callback 开头的完整 URL"
|
||||
},
|
||||
"remove_duplicate_keys": "移除重复密钥",
|
||||
"remove_invalid_keys": "删除无效密钥",
|
||||
|
||||
@ -4573,7 +4573,25 @@
|
||||
"official_website": "官方網站"
|
||||
},
|
||||
"openai": {
|
||||
"alert": "OpenAI Provider 不再支援舊的呼叫方法。如果使用第三方 API,請建立新的服務供應商"
|
||||
"alert": "OpenAI Provider 不再支援舊的呼叫方法。如果使用第三方 API,請建立新的服務供應商",
|
||||
"apikey": "API key",
|
||||
"auth_failed": "OpenAI OAuth 認證失敗",
|
||||
"auth_method": "認證方式",
|
||||
"auth_success": "OpenAI OAuth 認證成功",
|
||||
"authenticated": "已認證",
|
||||
"authenticating": "正在認證",
|
||||
"cancel": "取消",
|
||||
"description": "使用 OpenAI OAuth 登入",
|
||||
"description_detail": "點擊開始授權後會在瀏覽器開啟 OpenAI 登入頁。登入後請複製跳轉至本機位址的完整 URL(http://localhost:1455/auth/callback?...),貼回此處完成認證。",
|
||||
"enter_redirect_url": "重新導向 URL",
|
||||
"logout": "登出",
|
||||
"logout_failed": "登出失敗",
|
||||
"logout_success": "已登出",
|
||||
"oauth": "網頁 OAuth",
|
||||
"start_auth": "開始授權",
|
||||
"submit_url": "提交 URL",
|
||||
"url_error": "URL 解析或認證失敗,請重試",
|
||||
"url_placeholder": "貼上以 http://localhost:1455/auth/callback 開頭的完整 URL"
|
||||
},
|
||||
"remove_duplicate_keys": "移除重複金鑰",
|
||||
"remove_invalid_keys": "刪除無效金鑰",
|
||||
|
||||
@ -4573,7 +4573,25 @@
|
||||
"official_website": "Offizielle Website"
|
||||
},
|
||||
"openai": {
|
||||
"alert": "OpenAI-Anbieter unterstützt keine alten Aufrufmethoden mehr, wenn Sie einen Drittanbieter-API verwenden, erstellen Sie bitte einen neuen Anbieter"
|
||||
"alert": "OpenAI-Anbieter unterstützt keine alten Aufrufmethoden mehr, wenn Sie einen Drittanbieter-API verwenden, erstellen Sie bitte einen neuen Anbieter",
|
||||
"apikey": "API-Schlüssel",
|
||||
"auth_failed": "OpenAI-OAuth-Authentifizierung fehlgeschlagen",
|
||||
"auth_method": "Authentifizierungsmethode",
|
||||
"auth_success": "OpenAI-OAuth-Authentifizierung erfolgreich",
|
||||
"authenticated": "Authentifiziert",
|
||||
"authenticating": "Authentifizierung läuft",
|
||||
"cancel": "Abbrechen",
|
||||
"description": "OAuth-Authentifizierung",
|
||||
"description_detail": "Klicken Sie auf \"Autorisierung starten\", um die OpenAI-Anmeldeseite in Ihrem Browser zu öffnen. Nach dem Anmelden kopieren Sie die vollständige URL, die zur lokalen Adresse (http://localhost:1455/auth/callback?...) weiterleitet, und fügen Sie sie hier ein, um die Authentifizierung abzuschließen.",
|
||||
"enter_redirect_url": "Weiterleitungs-URL",
|
||||
"logout": "Abmelden",
|
||||
"logout_failed": "Abmelden fehlgeschlagen",
|
||||
"logout_success": "Abgemeldet",
|
||||
"oauth": "Web-OAuth",
|
||||
"start_auth": "Autorisierung starten",
|
||||
"submit_url": "URL senden",
|
||||
"url_error": "URL-Analyse oder Authentifizierung fehlgeschlagen, bitte erneut versuchen",
|
||||
"url_placeholder": "Fügen Sie die vollständige URL ein, die mit http://localhost:1455/auth/callback beginnt"
|
||||
},
|
||||
"remove_duplicate_keys": "Doppelte Schlüssel entfernen",
|
||||
"remove_invalid_keys": "Ungültige Schlüssel löschen",
|
||||
|
||||
@ -4573,7 +4573,25 @@
|
||||
"official_website": "Επίσημη ιστοσελίδα"
|
||||
},
|
||||
"openai": {
|
||||
"alert": "Ο πάροχος OpenAI δεν υποστηρίζει πλέον την παλιά μέθοδο κλήσης, παρακαλώ δημιουργήστε έναν νέο πάροχο API αν χρησιμοποιείτε τρίτους"
|
||||
"alert": "Ο πάροχος OpenAI δεν υποστηρίζει πλέον την παλιά μέθοδο κλήσης, παρακαλώ δημιουργήστε έναν νέο πάροχο API αν χρησιμοποιείτε τρίτους",
|
||||
"apikey": "Κλειδί API",
|
||||
"auth_failed": "Αποτυχία ελέγχου ταυτότητας OpenAI OAuth",
|
||||
"auth_method": "Μέθοδος ελέγχου ταυτότητας",
|
||||
"auth_success": "Επιτυχής έλεγχος ταυτότητας OpenAI OAuth",
|
||||
"authenticated": "Έγινε έλεγχος ταυτότητας",
|
||||
"authenticating": "Γίνεται έλεγχος ταυτότητας",
|
||||
"cancel": "Ακύρωση",
|
||||
"description": "Έλεγχος ταυτότητας OAuth",
|
||||
"description_detail": "Κάντε κλικ στο \"Έναρξη εξουσιοδότησης\" για να ανοίξετε τη σελίδα σύνδεσης της OpenAI στον περιηγητή σας. Αφού συνδεθείτε, αντιγράψτε ολόκληρο το URL που ανακατευθύνει στη τοπική διεύθυνση (http://localhost:1455/auth/callback?...) και επικολλήστε το εδώ για να ολοκληρώσετε τον έλεγχο ταυτότητας.",
|
||||
"enter_redirect_url": "URL ανακατεύθυνσης",
|
||||
"logout": "Αποσύνδεση",
|
||||
"logout_failed": "Αποτυχία αποσύνδεσης",
|
||||
"logout_success": "Έγινε αποσύνδεση",
|
||||
"oauth": "Web OAuth",
|
||||
"start_auth": "Έναρξη εξουσιοδότησης",
|
||||
"submit_url": "Υποβολή URL",
|
||||
"url_error": "Αποτυχία ανάλυσης URL ή ελέγχου ταυτότητας, δοκιμάστε ξανά",
|
||||
"url_placeholder": "Επικολλήστε το πλήρες URL που ξεκινά με http://localhost:1455/auth/callback"
|
||||
},
|
||||
"remove_duplicate_keys": "Αφαίρεση Επαναλαμβανόμενων Κλειδιών",
|
||||
"remove_invalid_keys": "Διαγραφή Ακυρωμένων Κλειδιών",
|
||||
|
||||
@ -4573,7 +4573,25 @@
|
||||
"official_website": "Sitio web oficial"
|
||||
},
|
||||
"openai": {
|
||||
"alert": "El proveedor de OpenAI ya no admite el método de llamada antiguo; si utiliza una API de terceros, cree un nuevo proveedor"
|
||||
"alert": "El proveedor de OpenAI ya no admite el método de llamada antiguo; si utiliza una API de terceros, cree un nuevo proveedor",
|
||||
"apikey": "Clave API",
|
||||
"auth_failed": "Falló la autenticación OAuth de OpenAI",
|
||||
"auth_method": "Método de autenticación",
|
||||
"auth_success": "Autenticación OAuth de OpenAI exitosa",
|
||||
"authenticated": "Autenticado",
|
||||
"authenticating": "Autenticando",
|
||||
"cancel": "Cancelar",
|
||||
"description": "Autenticación OAuth",
|
||||
"description_detail": "Haz clic en \"Iniciar autorización\" para abrir la página de inicio de sesión de OpenAI en tu navegador. Después de iniciar sesión, copia la URL completa que redirige a la dirección local (http://localhost:1455/auth/callback?...) y pégala aquí para completar la autenticación.",
|
||||
"enter_redirect_url": "URL de redirección",
|
||||
"logout": "Cerrar sesión",
|
||||
"logout_failed": "Error al cerrar sesión",
|
||||
"logout_success": "Sesión cerrada",
|
||||
"oauth": "OAuth web",
|
||||
"start_auth": "Iniciar autorización",
|
||||
"submit_url": "Enviar URL",
|
||||
"url_error": "Error al analizar la URL o al autenticar; inténtalo de nuevo",
|
||||
"url_placeholder": "Pega la URL completa que empieza por http://localhost:1455/auth/callback"
|
||||
},
|
||||
"remove_duplicate_keys": "Eliminar claves duplicadas",
|
||||
"remove_invalid_keys": "Eliminar claves inválidas",
|
||||
|
||||
@ -4573,7 +4573,25 @@
|
||||
"official_website": "Официальный сайт"
|
||||
},
|
||||
"openai": {
|
||||
"alert": "Le fournisseur OpenAI ne prend plus en charge l'ancienne méthode d'appel. Veuillez créer un nouveau fournisseur si vous utilisez une API tierce"
|
||||
"alert": "Le fournisseur OpenAI ne prend plus en charge l'ancienne méthode d'appel. Veuillez créer un nouveau fournisseur si vous utilisez une API tierce",
|
||||
"apikey": "Clé API",
|
||||
"auth_failed": "Échec de l’authentification OAuth OpenAI",
|
||||
"auth_method": "Méthode d’authentification",
|
||||
"auth_success": "Authentification OAuth OpenAI réussie",
|
||||
"authenticated": "Authentifié",
|
||||
"authenticating": "Authentification en cours",
|
||||
"cancel": "Annuler",
|
||||
"description": "Authentification OAuth",
|
||||
"description_detail": "Cliquez sur « Démarrer l’autorisation » pour ouvrir la page de connexion OpenAI dans votre navigateur. Après vous être connecté, copiez l’URL complète qui redirige vers l’adresse locale (http://localhost:1455/auth/callback?...) et collez-la ici pour terminer l’authentification.",
|
||||
"enter_redirect_url": "URL de redirection",
|
||||
"logout": "Se déconnecter",
|
||||
"logout_failed": "Échec de la déconnexion",
|
||||
"logout_success": "Déconnecté",
|
||||
"oauth": "OAuth Web",
|
||||
"start_auth": "Démarrer l’autorisation",
|
||||
"submit_url": "Envoyer l’URL",
|
||||
"url_error": "L’analyse de l’URL ou l’authentification a échoué, veuillez réessayer",
|
||||
"url_placeholder": "Collez l’URL complète commençant par http://localhost:1455/auth/callback"
|
||||
},
|
||||
"remove_duplicate_keys": "Supprimer les clés en double",
|
||||
"remove_invalid_keys": "Supprimer les clés invalides",
|
||||
|
||||
@ -4573,7 +4573,25 @@
|
||||
"official_website": "公式サイト"
|
||||
},
|
||||
"openai": {
|
||||
"alert": "OpenAIプロバイダーは旧式の呼び出し方法をサポートしなくなりました。サードパーティのAPIを使用している場合は、新しいサービスプロバイダーを作成してください。"
|
||||
"alert": "OpenAIプロバイダーは旧式の呼び出し方法をサポートしなくなりました。サードパーティのAPIを使用している場合は、新しいサービスプロバイダーを作成してください。",
|
||||
"apikey": "APIキー",
|
||||
"auth_failed": "OpenAI OAuth認証に失敗しました",
|
||||
"auth_method": "認証方法",
|
||||
"auth_success": "OpenAI OAuth認証に成功しました",
|
||||
"authenticated": "認証済み",
|
||||
"authenticating": "認証中",
|
||||
"cancel": "キャンセル",
|
||||
"description": "OAuth認証",
|
||||
"description_detail": "「認可を開始」をクリックして、ブラウザで OpenAI のログインページを開きます。ログイン後、ローカルアドレス(http://localhost:1455/auth/callback?...)にリダイレクトされる完全なURLをコピーし、ここに貼り付けて認証を完了してください。",
|
||||
"enter_redirect_url": "リダイレクトURL",
|
||||
"logout": "ログアウト",
|
||||
"logout_failed": "ログアウトに失敗しました",
|
||||
"logout_success": "ログアウトしました",
|
||||
"oauth": "Web OAuth",
|
||||
"start_auth": "認可を開始",
|
||||
"submit_url": "URLを送信",
|
||||
"url_error": "URLの解析または認証に失敗しました。もう一度お試しください",
|
||||
"url_placeholder": "http://localhost:1455/auth/callback で始まる完全なURLを貼り付けてください"
|
||||
},
|
||||
"remove_duplicate_keys": "重複キーを削除",
|
||||
"remove_invalid_keys": "無効なキーを削除",
|
||||
|
||||
@ -4573,7 +4573,25 @@
|
||||
"official_website": "Site Oficial"
|
||||
},
|
||||
"openai": {
|
||||
"alert": "O provedor OpenAI não suporta mais o método antigo de chamada. Se estiver usando uma API de terceiros, crie um novo provedor"
|
||||
"alert": "O provedor OpenAI não suporta mais o método antigo de chamada. Se estiver usando uma API de terceiros, crie um novo provedor",
|
||||
"apikey": "Chave da API",
|
||||
"auth_failed": "Falha na autenticação OAuth da OpenAI",
|
||||
"auth_method": "Método de autenticação",
|
||||
"auth_success": "Autenticação OAuth da OpenAI bem-sucedida",
|
||||
"authenticated": "Autenticado",
|
||||
"authenticating": "A autenticar",
|
||||
"cancel": "Cancelar",
|
||||
"description": "Autenticação OAuth",
|
||||
"description_detail": "Clique em \"Iniciar autorização\" para abrir a página de início de sessão da OpenAI no seu navegador. Depois de iniciar sessão, copie o URL completo que redireciona para o endereço local (http://localhost:1455/auth/callback?...) e cole-o aqui para concluir a autenticação.",
|
||||
"enter_redirect_url": "URL de redirecionamento",
|
||||
"logout": "Terminar sessão",
|
||||
"logout_failed": "Falha ao terminar sessão",
|
||||
"logout_success": "Sessão terminada",
|
||||
"oauth": "OAuth Web",
|
||||
"start_auth": "Iniciar autorização",
|
||||
"submit_url": "Enviar URL",
|
||||
"url_error": "Análise do URL ou autenticação falhou, tente novamente",
|
||||
"url_placeholder": "Cole o URL completo começando por http://localhost:1455/auth/callback"
|
||||
},
|
||||
"remove_duplicate_keys": "Remover chaves duplicadas",
|
||||
"remove_invalid_keys": "Remover chaves inválidas",
|
||||
|
||||
@ -4573,7 +4573,25 @@
|
||||
"official_website": "Официальный сайт"
|
||||
},
|
||||
"openai": {
|
||||
"alert": "Поставщик OpenAI больше не поддерживает старые методы вызова. Если вы используете сторонний API, создайте нового поставщика услуг."
|
||||
"alert": "Поставщик OpenAI больше не поддерживает старые методы вызова. Если вы используете сторонний API, создайте нового поставщика услуг.",
|
||||
"apikey": "Ключ API",
|
||||
"auth_failed": "Сбой аутентификации OpenAI OAuth",
|
||||
"auth_method": "Метод аутентификации",
|
||||
"auth_success": "Аутентификация OpenAI OAuth выполнена успешно",
|
||||
"authenticated": "Аутентифицировано",
|
||||
"authenticating": "Идёт аутентификация",
|
||||
"cancel": "Отмена",
|
||||
"description": "Аутентификация OAuth",
|
||||
"description_detail": "Нажмите \"Начать авторизацию\", чтобы открыть страницу входа OpenAI в вашем браузере. После входа скопируйте полный URL, который перенаправляет на локальный адрес (http://localhost:1455/auth/callback?...), и вставьте его здесь, чтобы завершить аутентификацию.",
|
||||
"enter_redirect_url": "URL перенаправления",
|
||||
"logout": "Выйти",
|
||||
"logout_failed": "Не удалось выйти",
|
||||
"logout_success": "Выполнен выход",
|
||||
"oauth": "Веб‑OAuth",
|
||||
"start_auth": "Начать авторизацию",
|
||||
"submit_url": "Отправить URL",
|
||||
"url_error": "Не удалось разобрать URL или выполнить аутентификацию. Повторите попытку",
|
||||
"url_placeholder": "Вставьте полный URL, начинающийся с http://localhost:1455/auth/callback"
|
||||
},
|
||||
"remove_duplicate_keys": "Удалить дубликаты ключей",
|
||||
"remove_invalid_keys": "Удалить недействительные ключи",
|
||||
|
||||
@ -0,0 +1,158 @@
|
||||
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
|
||||
@ -58,6 +58,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'
|
||||
@ -384,7 +385,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>
|
||||
@ -419,7 +420,21 @@ 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' && (
|
||||
@ -437,7 +452,7 @@ const ProviderSetting: FC<Props> = ({ providerId }) => {
|
||||
{provider.authType === 'oauth' && <AnthropicSettings />}
|
||||
</>
|
||||
)}
|
||||
{!hideApiInput && !isAnthropicOAuth() && (
|
||||
{!hideApiInput && !isAnthropicOAuth() && !isOpenAIOAuth() && (
|
||||
<>
|
||||
{!hideApiKeyInput && (
|
||||
<>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user