mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-10 15:49:29 +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_CLEAN_LOCAL_DATA = 'trace:cleanLocalData',
|
||||||
TRACE_ADD_STREAM_MESSAGE = 'trace:addStreamMessage',
|
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
|
||||||
CodeTools_Run = 'code-tools:run',
|
CodeTools_Run = 'code-tools:run',
|
||||||
|
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import path from 'node:path'
|
|||||||
import { loggerService } from '@logger'
|
import { loggerService } from '@logger'
|
||||||
import { isLinux, isMac, isPortable, isWin } from '@main/constant'
|
import { isLinux, isMac, isPortable, isWin } from '@main/constant'
|
||||||
import { generateSignature } from '@main/integration/cherryin'
|
import { generateSignature } from '@main/integration/cherryin'
|
||||||
|
import anthropicService from '@main/services/AnthropicService'
|
||||||
import { getBinaryPath, isBinaryExists, runInstallScript } from '@main/utils/process'
|
import { getBinaryPath, isBinaryExists, runInstallScript } from '@main/utils/process'
|
||||||
import { handleZoomFactor } from '@main/utils/zoom'
|
import { handleZoomFactor } from '@main/utils/zoom'
|
||||||
import { SpanEntity, TokenUsage } from '@mcp-trace/trace-core'
|
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)
|
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
|
// CodeTools
|
||||||
ipcMain.handle(IpcChannel.CodeTools_Run, codeToolsService.run)
|
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) =>
|
addStreamMessage: (spanId: string, modelName: string, context: string, message: any) =>
|
||||||
ipcRenderer.invoke(IpcChannel.TRACE_ADD_STREAM_MESSAGE, spanId, modelName, context, message)
|
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: {
|
codeTools: {
|
||||||
run: (
|
run: (
|
||||||
cliTool: string,
|
cliTool: string,
|
||||||
|
|||||||
@ -21,6 +21,7 @@ import LegacyAiProvider from './legacy/index'
|
|||||||
import { CompletionsParams, CompletionsResult } from './legacy/middleware/schemas'
|
import { CompletionsParams, CompletionsResult } from './legacy/middleware/schemas'
|
||||||
import { AiSdkMiddlewareConfig, buildAiSdkMiddlewares } from './middleware/AiSdkMiddlewareBuilder'
|
import { AiSdkMiddlewareConfig, buildAiSdkMiddlewares } from './middleware/AiSdkMiddlewareBuilder'
|
||||||
import { buildPlugins } from './plugins/PluginBuilder'
|
import { buildPlugins } from './plugins/PluginBuilder'
|
||||||
|
import { buildClaudeCodeSystemMessage } from './provider/config/anthropic'
|
||||||
import { createAiSdkProvider } from './provider/factory'
|
import { createAiSdkProvider } from './provider/factory'
|
||||||
import {
|
import {
|
||||||
getActualProvider,
|
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()) {
|
if (config.topicId && getEnableDeveloperMode()) {
|
||||||
// TypeScript类型窄化:确保topicId是string类型
|
// TypeScript类型窄化:确保topicId是string类型
|
||||||
const traceConfig = {
|
const traceConfig = {
|
||||||
|
|||||||
@ -85,6 +85,8 @@ export class AnthropicAPIClient extends BaseApiClient<
|
|||||||
ToolUseBlock,
|
ToolUseBlock,
|
||||||
ToolUnion
|
ToolUnion
|
||||||
> {
|
> {
|
||||||
|
oauthToken: string | undefined = undefined
|
||||||
|
isOAuthMode: boolean = false
|
||||||
sdkInstance: Anthropic | AnthropicVertex | undefined = undefined
|
sdkInstance: Anthropic | AnthropicVertex | undefined = undefined
|
||||||
constructor(provider: Provider) {
|
constructor(provider: Provider) {
|
||||||
super(provider)
|
super(provider)
|
||||||
@ -94,22 +96,79 @@ export class AnthropicAPIClient extends BaseApiClient<
|
|||||||
if (this.sdkInstance) {
|
if (this.sdkInstance) {
|
||||||
return this.sdkInstance
|
return this.sdkInstance
|
||||||
}
|
}
|
||||||
this.sdkInstance = new Anthropic({
|
|
||||||
apiKey: this.apiKey,
|
if (this.provider.authType === 'oauth') {
|
||||||
baseURL: this.getBaseURL(),
|
if (!this.oauthToken) {
|
||||||
dangerouslyAllowBrowser: true,
|
throw new Error('OAuth token is not available')
|
||||||
defaultHeaders: {
|
|
||||||
'anthropic-beta': 'output-128k-2025-02-19',
|
|
||||||
...this.provider.extra_headers
|
|
||||||
}
|
}
|
||||||
})
|
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
|
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(
|
override async createCompletions(
|
||||||
payload: AnthropicSdkParams,
|
payload: AnthropicSdkParams,
|
||||||
options?: Anthropic.RequestOptions
|
options?: Anthropic.RequestOptions
|
||||||
): Promise<AnthropicSdkRawOutput> {
|
): 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
|
const sdk = (await this.getSdkInstance()) as Anthropic
|
||||||
if (payload.stream) {
|
if (payload.stream) {
|
||||||
return sdk.messages.stream(payload, options)
|
return sdk.messages.stream(payload, options)
|
||||||
@ -124,8 +183,14 @@ export class AnthropicAPIClient extends BaseApiClient<
|
|||||||
}
|
}
|
||||||
|
|
||||||
override async listModels(): Promise<Anthropic.ModelInfo[]> {
|
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 sdk = (await this.getSdkInstance()) as Anthropic
|
||||||
const response = await sdk.models.list()
|
const response = await sdk.models.list()
|
||||||
|
|
||||||
return response.data
|
return response.data
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -194,7 +259,6 @@ export class AnthropicAPIClient extends BaseApiClient<
|
|||||||
/**
|
/**
|
||||||
* Get the message parameter
|
* Get the message parameter
|
||||||
* @param message - The message
|
* @param message - The message
|
||||||
* @param model - The model
|
|
||||||
* @returns The message parameter
|
* @returns The message parameter
|
||||||
*/
|
*/
|
||||||
public async convertMessageToSdkParam(message: Message): Promise<AnthropicSdkMessageParam> {
|
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,
|
provider: Provider,
|
||||||
config: ReturnType<typeof providerToAiSdkConfig>
|
config: ReturnType<typeof providerToAiSdkConfig>
|
||||||
) {
|
) {
|
||||||
if (provider.id === 'copilot') {
|
switch (provider.id) {
|
||||||
const defaultHeaders = store.getState().copilot.defaultHeaders
|
case 'copilot': {
|
||||||
const { token } = await window.api.copilot.getToken(defaultHeaders)
|
const defaultHeaders = store.getState().copilot.defaultHeaders
|
||||||
config.options.apiKey = token
|
const { token } = await window.api.copilot.getToken(defaultHeaders)
|
||||||
}
|
config.options.apiKey = token
|
||||||
if (provider.id === 'cherryin') {
|
break
|
||||||
config.options.fetch = async (url, options) => {
|
}
|
||||||
// 在这里对最终参数进行签名
|
case 'cherryin': {
|
||||||
const signature = await window.api.cherryin.generateSignature({
|
config.options.fetch = async (url, options) => {
|
||||||
method: 'POST',
|
// 在这里对最终参数进行签名
|
||||||
path: '/chat/completions',
|
const signature = await window.api.cherryin.generateSignature({
|
||||||
query: '',
|
method: 'POST',
|
||||||
body: JSON.parse(options.body)
|
path: '/chat/completions',
|
||||||
})
|
query: '',
|
||||||
return fetch(url, {
|
body: JSON.parse(options.body)
|
||||||
...options,
|
})
|
||||||
headers: {
|
return fetch(url, {
|
||||||
...options.headers,
|
...options,
|
||||||
...signature
|
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
|
return config
|
||||||
|
|||||||
@ -3649,6 +3649,27 @@
|
|||||||
"title": "Add Provider",
|
"title": "Add Provider",
|
||||||
"type": "Provider Type"
|
"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": {
|
"api": {
|
||||||
"key": {
|
"key": {
|
||||||
"check": {
|
"check": {
|
||||||
|
|||||||
@ -3649,6 +3649,27 @@
|
|||||||
"title": "添加提供商",
|
"title": "添加提供商",
|
||||||
"type": "提供商类型"
|
"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": {
|
"api": {
|
||||||
"key": {
|
"key": {
|
||||||
"check": {
|
"check": {
|
||||||
|
|||||||
@ -3649,6 +3649,27 @@
|
|||||||
"title": "新增提供者",
|
"title": "新增提供者",
|
||||||
"type": "供應商類型"
|
"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": {
|
"api": {
|
||||||
"key": {
|
"key": {
|
||||||
"check": {
|
"check": {
|
||||||
|
|||||||
@ -3649,6 +3649,23 @@
|
|||||||
"title": "Προσθήκη παρόχου",
|
"title": "Προσθήκη παρόχου",
|
||||||
"type": "Τύπος παρόχου"
|
"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": {
|
"api": {
|
||||||
"key": {
|
"key": {
|
||||||
"check": {
|
"check": {
|
||||||
|
|||||||
@ -3649,6 +3649,23 @@
|
|||||||
"title": "Agregar proveedor",
|
"title": "Agregar proveedor",
|
||||||
"type": "Tipo de 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": {
|
"api": {
|
||||||
"key": {
|
"key": {
|
||||||
"check": {
|
"check": {
|
||||||
|
|||||||
@ -3649,6 +3649,23 @@
|
|||||||
"title": "Ajouter un fournisseur",
|
"title": "Ajouter un fournisseur",
|
||||||
"type": "Type de 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": {
|
"api": {
|
||||||
"key": {
|
"key": {
|
||||||
"check": {
|
"check": {
|
||||||
|
|||||||
@ -3649,6 +3649,27 @@
|
|||||||
"title": "プロバイダーを追加",
|
"title": "プロバイダーを追加",
|
||||||
"type": "プロバイダータイプ"
|
"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": {
|
"api": {
|
||||||
"key": {
|
"key": {
|
||||||
"check": {
|
"check": {
|
||||||
|
|||||||
@ -3649,6 +3649,23 @@
|
|||||||
"title": "Adicionar Fornecedor",
|
"title": "Adicionar Fornecedor",
|
||||||
"type": "Tipo de 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": {
|
"api": {
|
||||||
"key": {
|
"key": {
|
||||||
"check": {
|
"check": {
|
||||||
|
|||||||
@ -3649,6 +3649,27 @@
|
|||||||
"title": "Добавить провайдер",
|
"title": "Добавить провайдер",
|
||||||
"type": "Тип провайдера"
|
"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": {
|
"api": {
|
||||||
"key": {
|
"key": {
|
||||||
"check": {
|
"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 { useAllProviders, useProvider, useProviders } from '@renderer/hooks/useProvider'
|
||||||
import { useTimer } from '@renderer/hooks/useTimer'
|
import { useTimer } from '@renderer/hooks/useTimer'
|
||||||
import i18n from '@renderer/i18n'
|
import i18n from '@renderer/i18n'
|
||||||
|
import AnthropicSettings from '@renderer/pages/settings/ProviderSettings/AnthropicSettings'
|
||||||
import { ModelList } from '@renderer/pages/settings/ProviderSettings/ModelList'
|
import { ModelList } from '@renderer/pages/settings/ProviderSettings/ModelList'
|
||||||
import { checkApi } from '@renderer/services/ApiService'
|
import { checkApi } from '@renderer/services/ApiService'
|
||||||
import { isProviderSupportAuth } from '@renderer/services/ProviderService'
|
import { isProviderSupportAuth } from '@renderer/services/ProviderService'
|
||||||
@ -17,7 +18,7 @@ import { isSystemProvider } from '@renderer/types'
|
|||||||
import { ApiKeyConnectivity, HealthStatus } from '@renderer/types/healthCheck'
|
import { ApiKeyConnectivity, HealthStatus } from '@renderer/types/healthCheck'
|
||||||
import { formatApiHost, formatApiKeys, getFancyProviderName, isOpenAIProvider } from '@renderer/utils'
|
import { formatApiHost, formatApiKeys, getFancyProviderName, isOpenAIProvider } from '@renderer/utils'
|
||||||
import { formatErrorMessage } from '@renderer/utils/error'
|
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 Link from 'antd/es/typography/Link'
|
||||||
import { debounce, isEmpty } from 'lodash'
|
import { debounce, isEmpty } from 'lodash'
|
||||||
import { Bolt, Check, Settings2, SquareArrowOutUpRight, TriangleAlert } from 'lucide-react'
|
import { Bolt, Check, Settings2, SquareArrowOutUpRight, TriangleAlert } from 'lucide-react'
|
||||||
@ -240,6 +241,8 @@ const ProviderSetting: FC<Props> = ({ providerId }) => {
|
|||||||
setApiHost(provider.apiHost)
|
setApiHost(provider.apiHost)
|
||||||
}, [provider.apiHost, provider.id])
|
}, [provider.apiHost, provider.id])
|
||||||
|
|
||||||
|
const isAnthropicOAuth = () => provider.id === 'anthropic' && provider.authType === 'oauth'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingContainer theme={theme} style={{ background: 'var(--color-background)' }}>
|
<SettingContainer theme={theme} style={{ background: 'var(--color-background)' }}>
|
||||||
<SettingTitle>
|
<SettingTitle>
|
||||||
@ -276,7 +279,22 @@ const ProviderSetting: FC<Props> = ({ providerId }) => {
|
|||||||
{isProviderSupportAuth(provider) && <ProviderOAuth providerId={provider.id} />}
|
{isProviderSupportAuth(provider) && <ProviderOAuth providerId={provider.id} />}
|
||||||
{provider.id === 'openai' && <OpenAIAlert />}
|
{provider.id === 'openai' && <OpenAIAlert />}
|
||||||
{isDmxapi && <DMXAPISettings providerId={provider.id} />}
|
{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
|
<SettingSubtitle
|
||||||
style={{
|
style={{
|
||||||
@ -328,7 +346,7 @@ const ProviderSetting: FC<Props> = ({ providerId }) => {
|
|||||||
<SettingHelpText>{t('settings.provider.api_key.tip')}</SettingHelpText>
|
<SettingHelpText>{t('settings.provider.api_key.tip')}</SettingHelpText>
|
||||||
</SettingHelpTextRow>
|
</SettingHelpTextRow>
|
||||||
)}
|
)}
|
||||||
{!isDmxapi && (
|
{!isDmxapi && !isAnthropicOAuth() && (
|
||||||
<>
|
<>
|
||||||
<SettingSubtitle style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
<SettingSubtitle style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||||
{t('settings.provider.api_host')}
|
{t('settings.provider.api_host')}
|
||||||
|
|||||||
@ -261,6 +261,7 @@ export type Provider = {
|
|||||||
/** @deprecated */
|
/** @deprecated */
|
||||||
isNotSupportServiceTier?: boolean
|
isNotSupportServiceTier?: boolean
|
||||||
|
|
||||||
|
authType?: 'apiKey' | 'oauth'
|
||||||
isVertex?: boolean
|
isVertex?: boolean
|
||||||
notes?: string
|
notes?: string
|
||||||
extra_headers?: Record<string, string>
|
extra_headers?: Record<string, string>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user