mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-28 21:42:27 +08:00
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:
parent
86e3776fff
commit
c940e0bc92
@ -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',
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
226
src/main/services/AnthropicService.ts
Normal file
226
src/main/services/AnthropicService.ts
Normal 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()
|
||||
@ -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,
|
||||
|
||||
@ -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 = {
|
||||
|
||||
@ -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> {
|
||||
|
||||
24
src/renderer/src/aiCore/provider/config/anthropic.ts
Normal file
24
src/renderer/src/aiCore/provider/config/anthropic.ts
Normal 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
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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
|
||||
@ -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')}
|
||||
|
||||
@ -261,6 +261,7 @@ export type Provider = {
|
||||
/** @deprecated */
|
||||
isNotSupportServiceTier?: boolean
|
||||
|
||||
authType?: 'apiKey' | 'oauth'
|
||||
isVertex?: boolean
|
||||
notes?: string
|
||||
extra_headers?: Record<string, string>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user