feat: add Anthropic OAuth settings UI and logic (#8905)

* feat: add Anthropic OAuth settings UI and logic

Introduces AnthropicSettings component for managing Anthropic OAuth authentication in provider settings. Adds Anthropic OAuth logic in a new anthropicOAuth.ts file, including PKCE flow, token exchange, and credential management stubs. Integrates AnthropicSettings into ProviderSetting to enable UI for login, logout, and code entry.

* feat: add Anthropic OAuth authentication support

Introduces OAuth authentication for Anthropic provider, including UI changes for selecting authentication method and handling authorization code input. Updates i18n files with new Anthropic OAuth-related strings in multiple languages and adds the 'authType' property to the Provider type.

* fix: oauth

* refactor: Anthropic OAuth to main process service

Moved Anthropic OAuth logic from renderer to main process as a singleton service. Updated IPC channels and preload API to support Anthropic OAuth actions. Refactored AnthropicSettings component to use new IPC-based API for authentication flow.

* fix: add 'authenticating' translation and update AnthropicSettings

Added the 'authenticating' key to Anthropic provider translations across multiple languages. Updated AnthropicSettings.tsx to remove the unused 'authenticating_detail' description and set the modal to be centered.

* fix: add reference

* Update AnthropicAPIClient.ts

* fix: update credentials path and improve OAuth handling in AnthropicAPIClient

* feat: add support for Anthropic OAuth provider handling in ProviderSetting

* feat: enhance OAuth authentication messages in multiple languages

* feat: add support for Anthropic provider with OAuth authentication and system message handling for new aisdk provider

* fix: update credential path and use net.fetch for OAuth token requests

* fix: setting page ui

---------

Co-authored-by: Vaayne <liu.vaayne@gmail.com>
This commit is contained in:
Pleasure1234 2025-09-05 17:43:20 +08:00 committed by GitHub
parent 86e3776fff
commit c940e0bc92
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 765 additions and 32 deletions

View File

@ -302,6 +302,14 @@ export enum IpcChannel {
TRACE_CLEAN_LOCAL_DATA = 'trace:cleanLocalData',
TRACE_ADD_STREAM_MESSAGE = 'trace:addStreamMessage',
// Anthropic OAuth
Anthropic_StartOAuthFlow = 'anthropic:start-oauth-flow',
Anthropic_CompleteOAuthWithCode = 'anthropic:complete-oauth-with-code',
Anthropic_CancelOAuthFlow = 'anthropic:cancel-oauth-flow',
Anthropic_GetAccessToken = 'anthropic:get-access-token',
Anthropic_HasCredentials = 'anthropic:has-credentials',
Anthropic_ClearCredentials = 'anthropic:clear-credentials',
// CodeTools
CodeTools_Run = 'code-tools:run',

View File

@ -5,6 +5,7 @@ import path from 'node:path'
import { loggerService } from '@logger'
import { isLinux, isMac, isPortable, isWin } from '@main/constant'
import { generateSignature } from '@main/integration/cherryin'
import anthropicService from '@main/services/AnthropicService'
import { getBinaryPath, isBinaryExists, runInstallScript } from '@main/utils/process'
import { handleZoomFactor } from '@main/utils/zoom'
import { SpanEntity, TokenUsage } from '@mcp-trace/trace-core'
@ -781,6 +782,16 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
addStreamMessage(spanId, modelName, context, msg)
)
// Anthropic OAuth
ipcMain.handle(IpcChannel.Anthropic_StartOAuthFlow, () => anthropicService.startOAuthFlow())
ipcMain.handle(IpcChannel.Anthropic_CompleteOAuthWithCode, (_, code: string) =>
anthropicService.completeOAuthWithCode(code)
)
ipcMain.handle(IpcChannel.Anthropic_CancelOAuthFlow, () => anthropicService.cancelOAuthFlow())
ipcMain.handle(IpcChannel.Anthropic_GetAccessToken, () => anthropicService.getValidAccessToken())
ipcMain.handle(IpcChannel.Anthropic_HasCredentials, () => anthropicService.hasCredentials())
ipcMain.handle(IpcChannel.Anthropic_ClearCredentials, () => anthropicService.clearCredentials())
// CodeTools
ipcMain.handle(IpcChannel.CodeTools_Run, codeToolsService.run)

View File

@ -0,0 +1,226 @@
/**
* Reference:
* This code is adapted from https://github.com/ThinkInAIXYZ/deepchat
* Original file: src/main/presenter/anthropicOAuth.ts
*/
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('AnthropicOAuth')
// Constants
const CLIENT_ID = '9d1c250a-e61b-44d9-88ed-5944d1962f5e'
const CREDS_PATH = path.join(getConfigDir(), 'oauth', 'anthropic.json')
// Types
interface Credentials {
access_token: string
refresh_token: string
expires_at: number
}
interface PKCEPair {
verifier: string
challenge: string
}
class AnthropicService extends Error {
private currentPKCE: PKCEPair | null = null
// 1. Generate PKCE pair
private generatePKCE(): PKCEPair {
const verifier = crypto.randomBytes(32).toString('base64url')
const challenge = crypto.createHash('sha256').update(verifier).digest('base64url')
return { verifier, challenge }
}
// 2. Get OAuth authorization URL
private getAuthorizationURL(pkce: PKCEPair): string {
const url = new URL('https://claude.ai/oauth/authorize')
url.searchParams.set('code', 'true')
url.searchParams.set('client_id', CLIENT_ID)
url.searchParams.set('response_type', 'code')
url.searchParams.set('redirect_uri', 'https://console.anthropic.com/oauth/code/callback')
url.searchParams.set('scope', 'org:create_api_key user:profile user:inference')
url.searchParams.set('code_challenge', pkce.challenge)
url.searchParams.set('code_challenge_method', 'S256')
url.searchParams.set('state', pkce.verifier)
return url.toString()
}
// 3. Exchange authorization code for tokens
private async exchangeCodeForTokens(code: string, verifier: string): Promise<Credentials> {
// Handle both legacy format (code#state) and new format (pure code)
const authCode = code.includes('#') ? code.split('#')[0] : code
const state = code.includes('#') ? code.split('#')[1] : verifier
const response = await net.fetch('https://console.anthropic.com/v1/oauth/token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
code: authCode,
state: state,
grant_type: 'authorization_code',
client_id: CLIENT_ID,
redirect_uri: 'https://console.anthropic.com/oauth/code/callback',
code_verifier: verifier
})
})
if (!response.ok) {
throw new Error(`Token exchange failed: ${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
}
}
// 4. Refresh access token
private async refreshAccessToken(refreshToken: string): Promise<Credentials> {
const response = await net.fetch('https://console.anthropic.com/v1/oauth/token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
grant_type: 'refresh_token',
refresh_token: refreshToken,
client_id: CLIENT_ID
})
})
if (!response.ok) {
throw new Error(`Token refresh failed: ${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
}
}
// 5. Save credentials
private async saveCredentials(creds: Credentials): Promise<void> {
await promises.mkdir(dirname(CREDS_PATH), { recursive: true })
await promises.writeFile(CREDS_PATH, JSON.stringify(creds, null, 2))
await promises.chmod(CREDS_PATH, 0o600) // Read/write for owner only
}
// 6. Load credentials
private async loadCredentials(): Promise<Credentials | null> {
try {
const data = await promises.readFile(CREDS_PATH, 'utf-8')
return JSON.parse(data)
} catch {
return null
}
}
// 7. Get valid access token (refresh if needed)
public async getValidAccessToken(): Promise<string | null> {
const creds = await this.loadCredentials()
if (!creds) return null
// If token is still valid, return it
if (creds.expires_at > Date.now() + 60000) {
// 1 minute buffer
return creds.access_token
}
// Otherwise, refresh it
try {
const newCreds = await this.refreshAccessToken(creds.refresh_token)
await this.saveCredentials(newCreds)
return newCreds.access_token
} catch {
return null
}
}
// 8. Start OAuth flow with external browser
public async startOAuthFlow(): Promise<string> {
// Try to get existing valid token
const existingToken = await this.getValidAccessToken()
if (existingToken) return existingToken
// Generate PKCE pair and store it for later use
this.currentPKCE = this.generatePKCE()
// Build authorization URL
const authUrl = this.getAuthorizationURL(this.currentPKCE)
logger.debug(authUrl)
// Open URL in external browser
await shell.openExternal(authUrl)
// Return the URL for UI to show (optional)
return authUrl
}
// 9. Complete OAuth flow with manual code input
public async completeOAuthWithCode(code: string): Promise<string> {
if (!this.currentPKCE) {
throw new Error('OAuth flow not started. Please call startOAuthFlow first.')
}
try {
// Exchange code for tokens using stored PKCE verifier
const credentials = await this.exchangeCodeForTokens(code, this.currentPKCE.verifier)
await this.saveCredentials(credentials)
// Clear stored PKCE after successful exchange
this.currentPKCE = null
return credentials.access_token
} catch (error) {
logger.error('OAuth code exchange failed:', error as Error)
// Clear PKCE on error
this.currentPKCE = null
throw error
}
}
// 10. Cancel current OAuth flow
public cancelOAuthFlow(): void {
if (this.currentPKCE) {
logger.info('Cancelling OAuth flow')
this.currentPKCE = null
}
}
// 11. Clear stored credentials
public async clearCredentials(): Promise<void> {
try {
await promises.unlink(CREDS_PATH)
logger.info('Credentials cleared')
} catch (error) {
// File doesn't exist, which is fine
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
throw error
}
}
}
// 12. Check if credentials exist
public async hasCredentials(): Promise<boolean> {
const creds = await this.loadCredentials()
return creds !== null
}
}
export default new AnthropicService()

View File

@ -422,6 +422,14 @@ const api = {
addStreamMessage: (spanId: string, modelName: string, context: string, message: any) =>
ipcRenderer.invoke(IpcChannel.TRACE_ADD_STREAM_MESSAGE, spanId, modelName, context, message)
},
anthropic_oauth: {
startOAuthFlow: () => ipcRenderer.invoke(IpcChannel.Anthropic_StartOAuthFlow),
completeOAuthWithCode: (code: string) => ipcRenderer.invoke(IpcChannel.Anthropic_CompleteOAuthWithCode, code),
cancelOAuthFlow: () => ipcRenderer.invoke(IpcChannel.Anthropic_CancelOAuthFlow),
getAccessToken: () => ipcRenderer.invoke(IpcChannel.Anthropic_GetAccessToken),
hasCredentials: () => ipcRenderer.invoke(IpcChannel.Anthropic_HasCredentials),
clearCredentials: () => ipcRenderer.invoke(IpcChannel.Anthropic_ClearCredentials)
},
codeTools: {
run: (
cliTool: string,

View File

@ -21,6 +21,7 @@ import LegacyAiProvider from './legacy/index'
import { CompletionsParams, CompletionsResult } from './legacy/middleware/schemas'
import { AiSdkMiddlewareConfig, buildAiSdkMiddlewares } from './middleware/AiSdkMiddlewareBuilder'
import { buildPlugins } from './plugins/PluginBuilder'
import { buildClaudeCodeSystemMessage } from './provider/config/anthropic'
import { createAiSdkProvider } from './provider/factory'
import {
getActualProvider,
@ -120,6 +121,16 @@ export default class ModernAiProvider {
}
}
if (this.actualProvider.id === 'anthropic' && this.actualProvider.authType === 'oauth') {
const claudeCodeSystemMessage = buildClaudeCodeSystemMessage(params.system)
params.system = undefined // 清除原有system避免重复
if (Array.isArray(params.messages)) {
params.messages = [...claudeCodeSystemMessage, ...params.messages]
} else {
params.messages = claudeCodeSystemMessage
}
}
if (config.topicId && getEnableDeveloperMode()) {
// TypeScript类型窄化确保topicId是string类型
const traceConfig = {

View File

@ -85,6 +85,8 @@ export class AnthropicAPIClient extends BaseApiClient<
ToolUseBlock,
ToolUnion
> {
oauthToken: string | undefined = undefined
isOAuthMode: boolean = false
sdkInstance: Anthropic | AnthropicVertex | undefined = undefined
constructor(provider: Provider) {
super(provider)
@ -94,22 +96,79 @@ export class AnthropicAPIClient extends BaseApiClient<
if (this.sdkInstance) {
return this.sdkInstance
}
this.sdkInstance = new Anthropic({
apiKey: this.apiKey,
baseURL: this.getBaseURL(),
dangerouslyAllowBrowser: true,
defaultHeaders: {
'anthropic-beta': 'output-128k-2025-02-19',
...this.provider.extra_headers
if (this.provider.authType === 'oauth') {
if (!this.oauthToken) {
throw new Error('OAuth token is not available')
}
})
this.sdkInstance = new Anthropic({
authToken: this.oauthToken,
baseURL: 'https://api.anthropic.com',
dangerouslyAllowBrowser: true,
defaultHeaders: {
'Content-Type': 'application/json',
'anthropic-version': '2023-06-01',
'anthropic-beta': 'oauth-2025-04-20'
// ...this.provider.extra_headers
}
})
} else {
this.sdkInstance = new Anthropic({
apiKey: this.apiKey,
baseURL: this.getBaseURL(),
dangerouslyAllowBrowser: true,
defaultHeaders: {
'anthropic-beta': 'output-128k-2025-02-19',
...this.provider.extra_headers
}
})
}
return this.sdkInstance
}
private buildClaudeCodeSystemMessage(system?: string | Array<TextBlockParam>): string | Array<TextBlockParam> {
const defaultClaudeCodeSystem = `You are Claude Code, Anthropic's official CLI for Claude.`
if (!system) {
return defaultClaudeCodeSystem
}
if (typeof system === 'string') {
if (system.trim() === defaultClaudeCodeSystem) {
return system
}
return [
{
type: 'text',
text: defaultClaudeCodeSystem
},
{
type: 'text',
text: system
}
]
}
if (system[0].text.trim() != defaultClaudeCodeSystem) {
system.unshift({
type: 'text',
text: defaultClaudeCodeSystem
})
}
return system
}
override async createCompletions(
payload: AnthropicSdkParams,
options?: Anthropic.RequestOptions
): Promise<AnthropicSdkRawOutput> {
if (this.provider.authType === 'oauth') {
this.oauthToken = await window.api.anthropic_oauth.getAccessToken()
this.isOAuthMode = true
logger.info('[Anthropic Provider] Using OAuth token for authentication')
payload.system = this.buildClaudeCodeSystemMessage(payload.system)
}
const sdk = (await this.getSdkInstance()) as Anthropic
if (payload.stream) {
return sdk.messages.stream(payload, options)
@ -124,8 +183,14 @@ export class AnthropicAPIClient extends BaseApiClient<
}
override async listModels(): Promise<Anthropic.ModelInfo[]> {
if (this.provider.authType === 'oauth') {
this.oauthToken = await window.api.anthropic_oauth.getAccessToken()
this.isOAuthMode = true
logger.info('[Anthropic Provider] Using OAuth token for authentication')
}
const sdk = (await this.getSdkInstance()) as Anthropic
const response = await sdk.models.list()
return response.data
}
@ -194,7 +259,6 @@ export class AnthropicAPIClient extends BaseApiClient<
/**
* Get the message parameter
* @param message - The message
* @param model - The model
* @returns The message parameter
*/
public async convertMessageToSdkParam(message: Message): Promise<AnthropicSdkMessageParam> {

View File

@ -0,0 +1,24 @@
import { SystemModelMessage } from 'ai'
export function buildClaudeCodeSystemMessage(system?: string): Array<SystemModelMessage> {
const defaultClaudeCodeSystem = `You are Claude Code, Anthropic's official CLI for Claude.`
if (!system || system.trim() === defaultClaudeCodeSystem) {
return [
{
role: 'system',
content: defaultClaudeCodeSystem
}
]
}
return [
{
role: 'system',
content: defaultClaudeCodeSystem
},
{
role: 'system',
content: system
}
]
}

View File

@ -240,27 +240,48 @@ export async function prepareSpecialProviderConfig(
provider: Provider,
config: ReturnType<typeof providerToAiSdkConfig>
) {
if (provider.id === 'copilot') {
const defaultHeaders = store.getState().copilot.defaultHeaders
const { token } = await window.api.copilot.getToken(defaultHeaders)
config.options.apiKey = token
}
if (provider.id === 'cherryin') {
config.options.fetch = async (url, options) => {
// 在这里对最终参数进行签名
const signature = await window.api.cherryin.generateSignature({
method: 'POST',
path: '/chat/completions',
query: '',
body: JSON.parse(options.body)
})
return fetch(url, {
...options,
headers: {
...options.headers,
...signature
switch (provider.id) {
case 'copilot': {
const defaultHeaders = store.getState().copilot.defaultHeaders
const { token } = await window.api.copilot.getToken(defaultHeaders)
config.options.apiKey = token
break
}
case 'cherryin': {
config.options.fetch = async (url, options) => {
// 在这里对最终参数进行签名
const signature = await window.api.cherryin.generateSignature({
method: 'POST',
path: '/chat/completions',
query: '',
body: JSON.parse(options.body)
})
return fetch(url, {
...options,
headers: {
...options.headers,
...signature
}
})
}
break
}
case 'anthropic': {
if (provider.authType === 'oauth') {
const oauthToken = await window.api.anthropic_oauth.getAccessToken()
config.options = {
...config.options,
headers: {
...(config.options.headers ? config.options.headers : {}),
'Content-Type': 'application/json',
'anthropic-version': '2023-06-01',
'anthropic-beta': 'oauth-2025-04-20',
Authorization: `Bearer ${oauthToken}`
},
baseURL: 'https://api.anthropic.com/v1',
apiKey: ''
}
})
}
}
}
return config

View File

@ -3649,6 +3649,27 @@
"title": "Add Provider",
"type": "Provider Type"
},
"anthropic": {
"apikey": "API key",
"auth_failed": "Anthropic authentication failed",
"auth_method": "Authentication method",
"auth_success": "Anthropic OAuth authentication successful",
"authenticated": "Verified",
"authenticating": "Authenticating",
"cancel": "Cancel",
"code_error": "Invalid authorization code, please try again",
"code_placeholder": "Please enter the authorization code displayed in your browser",
"code_required": "Authorization code cannot be empty",
"description": "OAuth authentication",
"description_detail": "You need to subscribe to Claude Pro or a higher version to use this authentication method",
"enter_auth_code": "Authorization code",
"logout": "Log out",
"logout_failed": "Logout failed, please try again",
"logout_success": "Successfully logged out of Anthropic",
"oauth": "Web OAuth",
"start_auth": "Start authorization",
"submit_code": "Complete login"
},
"api": {
"key": {
"check": {

View File

@ -3649,6 +3649,27 @@
"title": "添加提供商",
"type": "提供商类型"
},
"anthropic": {
"apikey": "API 密钥",
"auth_failed": "Anthropic 身份认证失败",
"auth_method": "认证方式",
"auth_success": "Anthropic OAuth 认证成功",
"authenticated": "已认证",
"authenticating": "正在认证",
"cancel": "取消",
"code_error": "无效的授权码,请重试",
"code_placeholder": "请输入浏览器中显示的授权码",
"code_required": "授权码不能为空",
"description": "OAuth 身份认证",
"description_detail": "你需要订阅 Claude Pro 或以上版本才能使用此认证方式",
"enter_auth_code": "授权码",
"logout": "退出登录",
"logout_failed": "退出登录失败,请重试",
"logout_success": "成功退出 Anthropic 登录",
"oauth": "网页 OAuth",
"start_auth": "开始授权",
"submit_code": "完成登录"
},
"api": {
"key": {
"check": {

View File

@ -3649,6 +3649,27 @@
"title": "新增提供者",
"type": "供應商類型"
},
"anthropic": {
"apikey": "API 密鑰",
"auth_failed": "Anthropic 身份驗證失敗",
"auth_method": "认证方式",
"auth_success": "Anthropic OAuth 認證成功",
"authenticated": "已認證",
"authenticating": "正在認證",
"cancel": "取消",
"code_error": "無效的授權碼,請重試",
"code_placeholder": "請輸入瀏覽器中顯示的授權碼",
"code_required": "授權碼不能為空",
"description": "OAuth 身份認證",
"description_detail": "您需要訂閱 Claude Pro 或更高版本才能使用此認證方式",
"enter_auth_code": "授權碼",
"logout": "登出",
"logout_failed": "登出失敗,請重試",
"logout_success": "成功登出 Anthropic",
"oauth": "網頁 OAuth",
"start_auth": "開始授權",
"submit_code": "完成登錄"
},
"api": {
"key": {
"check": {

View File

@ -3649,6 +3649,23 @@
"title": "Προσθήκη παρόχου",
"type": "Τύπος παρόχου"
},
"anthropic": {
"apikey": "Κλειδί API",
"auth_failed": "Αποτυχία πιστοποίησης ταυτότητας Anthropic",
"auth_method": "Τρόπος πιστοποίησης",
"authenticated": "Επαληθευμένο",
"authenticating": "Επαλήθευση σε εξέλιξη",
"cancel": "Ακύρωση",
"code_placeholder": "Παρακαλώ εισαγάγετε τον κωδικό εξουσιοδότησης που εμφανίζεται στον περιηγητή σας",
"code_required": "Ο κωδικός εξουσιοδότησης δεν μπορεί να είναι κενός",
"description": "Πιστοποίηση OAuth",
"description_detail": "Για να χρησιμοποιήσετε αυτόν τον τρόπο επαλήθευσης, πρέπει να είστε συνδρομητής Claude Pro ή έκδοσης μεγαλύτερης από αυτήν",
"enter_auth_code": "κωδικός εξουσιοδότησης",
"logout": "Αποσύνδεση λογαριασμού",
"oauth": "ιστοσελίδα OAuth",
"start_auth": "Έναρξη εξουσιοδότησης",
"submit_code": "Ολοκληρώστε την σύνδεση"
},
"api": {
"key": {
"check": {

View File

@ -3649,6 +3649,23 @@
"title": "Agregar proveedor",
"type": "Tipo de proveedor"
},
"anthropic": {
"apikey": "Clave de API",
"auth_failed": "Error de autenticación de Anthropic",
"auth_method": "Método de autenticación",
"authenticated": "Verificado",
"authenticating": "Autenticando",
"cancel": "Cancelar",
"code_placeholder": "Introduzca el código de autorización que se muestra en el navegador",
"code_required": "El código de autorización no puede estar vacío",
"description": "Autenticación OAuth",
"description_detail": "Necesitas suscribirte a Claude Pro o a una versión superior para utilizar este método de autenticación",
"enter_auth_code": "Código de autorización",
"logout": "Cerrar sesión",
"oauth": "Web OAuth",
"start_auth": "Comenzar autorización",
"submit_code": "Iniciar sesión completado"
},
"api": {
"key": {
"check": {

View File

@ -3649,6 +3649,23 @@
"title": "Ajouter un fournisseur",
"type": "Type de fournisseur"
},
"anthropic": {
"apikey": "Clé API",
"auth_failed": "Échec de l'authentification Anthropic",
"auth_method": "Mode d'authentification",
"authenticated": "Certifié",
"authenticating": "Authentification en cours",
"cancel": "Annuler",
"code_placeholder": "Veuillez saisir le code d'autorisation affiché dans le navigateur",
"code_required": "Le code d'autorisation ne peut pas être vide",
"description": "Authentification OAuth",
"description_detail": "Vous devez souscrire à Claude Pro ou à une version supérieure pour pouvoir utiliser cette méthode d'authentification.",
"enter_auth_code": "code d'autorisation",
"logout": "Déconnexion",
"oauth": "Authentification OAuth web",
"start_auth": "Commencer l'autorisation",
"submit_code": "Terminer la connexion"
},
"api": {
"key": {
"check": {

View File

@ -3649,6 +3649,27 @@
"title": "プロバイダーを追加",
"type": "プロバイダータイプ"
},
"anthropic": {
"apikey": "API キー",
"auth_failed": "Anthropic 身份验证に失敗しました",
"auth_method": "認証方法",
"auth_success": "Anthropic OAuth 認証が成功しました",
"authenticated": "認証済み",
"authenticating": "認証中です",
"cancel": "取消",
"code_error": "無効な認証コードです。もう一度お試しください",
"code_placeholder": "ブラウザに表示されている認証コードを入力してください",
"code_required": "認証コードは空にできません",
"description": "OAuth 認証",
"description_detail": "Claude Pro 以上にサブスクライブする必要があります。この認証方法を使用するには。",
"enter_auth_code": "認証コード",
"logout": "ログアウト",
"logout_failed": "ログアウトに失敗しました。もう一度お試しください",
"logout_success": "Anthropic からログアウトしました",
"oauth": "WebページOAuth",
"start_auth": "開始承認",
"submit_code": "ログインを完了する"
},
"api": {
"key": {
"check": {

View File

@ -3649,6 +3649,23 @@
"title": "Adicionar Fornecedor",
"type": "Tipo de Fornecedor"
},
"anthropic": {
"apikey": "Chave da API",
"auth_failed": "Falha na autenticação da Anthropic",
"auth_method": "Método de autenticação",
"authenticated": "[retranslating]: Verificado",
"authenticating": "A autenticar",
"cancel": "Cancelar",
"code_placeholder": "Introduza o código de autorização exibido no browser",
"code_required": "O código de autorização não pode estar vazio",
"description": "Autenticação OAuth",
"description_detail": "Precisa de uma subscrição Claude Pro ou superior para utilizar este método de autenticação",
"enter_auth_code": "Código de autorização",
"logout": "Sair da sessão",
"oauth": "OAuth da Página Web",
"start_auth": "Iniciar autorização",
"submit_code": "Concluir login"
},
"api": {
"key": {
"check": {

View File

@ -3649,6 +3649,27 @@
"title": "Добавить провайдер",
"type": "Тип провайдера"
},
"anthropic": {
"apikey": "API-ключ",
"auth_failed": "Ошибка аутентификации Anthropic",
"auth_method": "Способ аутентификации",
"auth_success": "Аутентификация Anthropic OAuth успешна",
"authenticated": "Подтверждено",
"authenticating": "Выполняется аутентификация",
"cancel": "Отмена",
"code_error": "Неверный код авторизации, попробуйте еще раз",
"code_placeholder": "Пожалуйста, введите код авторизации, отображаемый в браузере",
"code_required": "Код авторизации не может быть пустым",
"description": "OAuth аутентификация",
"description_detail": "Чтобы использовать этот способ аутентификации, вам необходимо подписаться на Claude Pro или более высокую версию.",
"enter_auth_code": "Код авторизации",
"logout": "Выйти",
"logout_failed": "Ошибка выхода, попробуйте еще раз",
"logout_success": "Успешно вышли из Anthropic",
"oauth": "веб OAuth",
"start_auth": "Начать авторизацию",
"submit_code": "Завершить вход"
},
"api": {
"key": {
"check": {

View File

@ -0,0 +1,168 @@
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('AnthropicSettings')
enum AuthStatus {
NOT_STARTED,
AUTHENTICATING,
AUTHENTICATED
}
const AnthropicSettings = () => {
const { t } = useTranslation()
const [authStatus, setAuthStatus] = useState<AuthStatus>(AuthStatus.NOT_STARTED)
const [loading, setLoading] = useState<boolean>(false)
const [codeModalVisible, setCodeModalVisible] = useState<boolean>(false)
const [authCode, setAuthCode] = useState<string>('')
// 初始化检查认证状态
useEffect(() => {
const checkAuthStatus = async () => {
try {
const hasCredentials = await window.api.anthropic_oauth.hasCredentials()
if (hasCredentials) {
setAuthStatus(AuthStatus.AUTHENTICATED)
}
} catch (error) {
logger.error('Failed to check authentication status:', error as Error)
}
}
checkAuthStatus()
}, [])
// 处理OAuth重定向
const handleRedirectOAuth = async () => {
try {
setLoading(true)
await window.api.anthropic_oauth.startOAuthFlow()
setAuthStatus(AuthStatus.AUTHENTICATING)
setCodeModalVisible(true)
} catch (error) {
logger.error('OAuth redirect failed:', error as Error)
window.message.error(t('settings.provider.anthropic.auth_failed'))
} finally {
setLoading(false)
}
}
// 处理授权码提交
const handleSubmitCode = async () => {
logger.info('Submitting auth code')
try {
setLoading(true)
await window.api.anthropic_oauth.completeOAuthWithCode(authCode)
setAuthStatus(AuthStatus.AUTHENTICATED)
setCodeModalVisible(false)
window.message.success(t('settings.provider.anthropic.auth_success'))
} catch (error) {
logger.error('Code submission failed:', error as Error)
window.message.error(t('settings.provider.anthropic.code_error'))
} finally {
setLoading(false)
}
}
// 处理取消认证
const handleCancelAuth = () => {
window.api.anthropic_oauth.cancelOAuthFlow()
setAuthStatus(AuthStatus.NOT_STARTED)
setCodeModalVisible(false)
setAuthCode('')
}
// 处理登出
const handleLogout = async () => {
try {
await window.api.anthropic_oauth.clearCredentials()
setAuthStatus(AuthStatus.NOT_STARTED)
window.message.success(t('settings.provider.anthropic.logout_success'))
} catch (error) {
logger.error('Logout failed:', error as Error)
window.message.error(t('settings.provider.anthropic.logout_failed'))
}
}
// 渲染认证内容
const renderAuthContent = () => {
switch (authStatus) {
case AuthStatus.AUTHENTICATED:
return (
<StartContainer>
<Alert
type="success"
message={t('settings.provider.anthropic.authenticated')}
action={
<Button type="primary" onClick={handleLogout}>
{t('settings.provider.anthropic.logout')}
</Button>
}
showIcon
icon={<ExclamationCircleOutlined />}
/>
</StartContainer>
)
case AuthStatus.AUTHENTICATING:
return (
<StartContainer>
<Alert
type="info"
message={t('settings.provider.anthropic.authenticating')}
showIcon
icon={<ExclamationCircleOutlined />}
/>
<Modal
title={t('settings.provider.anthropic.enter_auth_code')}
open={codeModalVisible}
onOk={handleSubmitCode}
onCancel={handleCancelAuth}
okButtonProps={{ loading }}
okText={t('settings.provider.anthropic.submit_code')}
cancelText={t('settings.provider.anthropic.cancel')}
centered>
<Input
value={authCode}
onChange={(e) => setAuthCode(e.target.value)}
placeholder={t('settings.provider.anthropic.code_placeholder')}
/>
</Modal>
</StartContainer>
)
default:
return (
<StartContainer>
<Alert
type="info"
message={t('settings.provider.anthropic.description')}
description={t('settings.provider.anthropic.description_detail')}
action={
<Button type="primary" loading={loading} onClick={handleRedirectOAuth}>
{t('settings.provider.anthropic.start_auth')}
</Button>
}
showIcon
icon={<ExclamationCircleOutlined />}
/>
</StartContainer>
)
}
}
return <Container>{renderAuthContent()}</Container>
}
const Container = styled.div`
padding-top: 10px;
`
const StartContainer = styled.div`
margin-bottom: 10px;
`
export default AnthropicSettings

View File

@ -8,6 +8,7 @@ import { useTheme } from '@renderer/context/ThemeProvider'
import { useAllProviders, useProvider, useProviders } from '@renderer/hooks/useProvider'
import { useTimer } from '@renderer/hooks/useTimer'
import i18n from '@renderer/i18n'
import AnthropicSettings from '@renderer/pages/settings/ProviderSettings/AnthropicSettings'
import { ModelList } from '@renderer/pages/settings/ProviderSettings/ModelList'
import { checkApi } from '@renderer/services/ApiService'
import { isProviderSupportAuth } from '@renderer/services/ProviderService'
@ -17,7 +18,7 @@ import { isSystemProvider } from '@renderer/types'
import { ApiKeyConnectivity, HealthStatus } from '@renderer/types/healthCheck'
import { formatApiHost, formatApiKeys, getFancyProviderName, isOpenAIProvider } from '@renderer/utils'
import { formatErrorMessage } from '@renderer/utils/error'
import { Button, Divider, Flex, Input, Space, Switch, Tooltip } from 'antd'
import { Button, Divider, Flex, Input, Select, Space, Switch, Tooltip } from 'antd'
import Link from 'antd/es/typography/Link'
import { debounce, isEmpty } from 'lodash'
import { Bolt, Check, Settings2, SquareArrowOutUpRight, TriangleAlert } from 'lucide-react'
@ -240,6 +241,8 @@ const ProviderSetting: FC<Props> = ({ providerId }) => {
setApiHost(provider.apiHost)
}, [provider.apiHost, provider.id])
const isAnthropicOAuth = () => provider.id === 'anthropic' && provider.authType === 'oauth'
return (
<SettingContainer theme={theme} style={{ background: 'var(--color-background)' }}>
<SettingTitle>
@ -276,7 +279,22 @@ const ProviderSetting: FC<Props> = ({ providerId }) => {
{isProviderSupportAuth(provider) && <ProviderOAuth providerId={provider.id} />}
{provider.id === 'openai' && <OpenAIAlert />}
{isDmxapi && <DMXAPISettings providerId={provider.id} />}
{!hideApiInput && (
{provider.id === 'anthropic' && (
<>
<SettingSubtitle style={{ marginTop: 5 }}>{t('settings.provider.anthropic.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.anthropic.apikey') },
{ value: 'oauth', label: t('settings.provider.anthropic.oauth') }
]}
/>
{provider.authType === 'oauth' && <AnthropicSettings />}
</>
)}
{!hideApiInput && !isAnthropicOAuth() && (
<>
<SettingSubtitle
style={{
@ -328,7 +346,7 @@ const ProviderSetting: FC<Props> = ({ providerId }) => {
<SettingHelpText>{t('settings.provider.api_key.tip')}</SettingHelpText>
</SettingHelpTextRow>
)}
{!isDmxapi && (
{!isDmxapi && !isAnthropicOAuth() && (
<>
<SettingSubtitle style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
{t('settings.provider.api_host')}

View File

@ -261,6 +261,7 @@ export type Provider = {
/** @deprecated */
isNotSupportServiceTier?: boolean
authType?: 'apiKey' | 'oauth'
isVertex?: boolean
notes?: string
extra_headers?: Record<string, string>