This commit is contained in:
LiBr 2025-12-18 20:29:34 +08:00 committed by GitHub
commit 1e9a0598c2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 724 additions and 14 deletions

View File

@ -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',

View File

@ -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())

View 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()

View File

@ -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

View File

@ -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",

View File

@ -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 登录页。登录后复制跳转到本地地址的整条 URLhttp://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": "删除无效密钥",

View File

@ -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 登入頁。登入後請複製跳轉至本機位址的完整 URLhttp://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": "刪除無效金鑰",

View File

@ -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",

View File

@ -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": "Διαγραφή Ακυρωμένων Κλειδιών",

View File

@ -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",

View File

@ -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 lauthentification OAuth OpenAI",
"auth_method": "Méthode dauthentification",
"auth_success": "Authentification OAuth OpenAI réussie",
"authenticated": "Authentifié",
"authenticating": "Authentification en cours",
"cancel": "Annuler",
"description": "Authentification OAuth",
"description_detail": "Cliquez sur « Démarrer lautorisation » pour ouvrir la page de connexion OpenAI dans votre navigateur. Après vous être connecté, copiez lURL complète qui redirige vers ladresse locale (http://localhost:1455/auth/callback?...) et collez-la ici pour terminer lauthentification.",
"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 lautorisation",
"submit_url": "Envoyer lURL",
"url_error": "Lanalyse de lURL ou lauthentification a échoué, veuillez réessayer",
"url_placeholder": "Collez lURL 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",

View File

@ -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": "無効なキーを削除",

View File

@ -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",

View File

@ -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": "Удалить недействительные ключи",

View File

@ -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

View File

@ -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 && (
<>