diff --git a/src/main/presenter/anthropicOAuth.ts b/src/main/presenter/anthropicOAuth.ts new file mode 100644 index 000000000..5f33bf2ca --- /dev/null +++ b/src/main/presenter/anthropicOAuth.ts @@ -0,0 +1,224 @@ +import { promises as fs } from 'fs' +import { homedir } from 'os' +import { join, dirname } from 'path' +import * as crypto from 'crypto' +import { shell } from 'electron' + +// Constants +const CLIENT_ID = '9d1c250a-e61b-44d9-88ed-5944d1962f5e' +const CREDS_PATH = join(homedir(), '.deepchat', 'credentials', 'anthropic.json') + +// Types +interface Credentials { + access_token: string + refresh_token: string + expires_at: number // timestamp in ms +} + +interface PKCEPair { + verifier: string + challenge: string +} + +export class AnthropicOAuth { + 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 { + // 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 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 { + const response = await 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 { + await fs.mkdir(dirname(CREDS_PATH), { recursive: true }) + await fs.writeFile(CREDS_PATH, JSON.stringify(creds, null, 2)) + await fs.chmod(CREDS_PATH, 0o600) // Read/write for owner only + } + + // 6. Load credentials + private async loadCredentials(): Promise { + try { + const data = await fs.readFile(CREDS_PATH, 'utf-8') + return JSON.parse(data) + } catch { + return null + } + } + + // 7. Get valid access token (refresh if needed) + public async getValidAccessToken(): Promise { + 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 { + // 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) + console.log('Opening OAuth URL in external browser:', 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 { + 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) { + console.error('OAuth code exchange failed:', error) + // Clear PKCE on error + this.currentPKCE = null + throw error + } + } + + // 10. Cancel current OAuth flow + public cancelOAuthFlow(): void { + if (this.currentPKCE) { + console.log('Cancelling OAuth flow') + this.currentPKCE = null + } + } + + // 11. Clear stored credentials + public async clearCredentials(): Promise { + try { + await fs.unlink(CREDS_PATH) + console.log('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 { + const creds = await this.loadCredentials() + return creds !== null + } +} + +// Create singleton instance +let instance: AnthropicOAuth | null = null + +export const createAnthropicOAuth = (): AnthropicOAuth => { + if (!instance) { + instance = new AnthropicOAuth() + } + return instance +} diff --git a/src/main/presenter/llmProviderPresenter/providers/anthropicProvider.ts b/src/main/presenter/llmProviderPresenter/providers/anthropicProvider.ts index 77bec6248..da77f773f 100644 --- a/src/main/presenter/llmProviderPresenter/providers/anthropicProvider.ts +++ b/src/main/presenter/llmProviderPresenter/providers/anthropicProvider.ts @@ -17,6 +17,8 @@ import { ProxyAgent } from 'undici' export class AnthropicProvider extends BaseLLMProvider { private anthropic!: Anthropic + private oauthToken?: string + private isOAuthMode = false private defaultModel = 'claude-3-7-sonnet-20250219' constructor(provider: LLM_PROVIDER, configPresenter: ConfigPresenter) { @@ -31,26 +33,81 @@ export class AnthropicProvider extends BaseLLMProvider { protected async init() { if (this.provider.enable) { try { - const apiKey = this.provider.apiKey || process.env.ANTHROPIC_API_KEY + let apiKey: string | null = null + this.isOAuthMode = false + this.oauthToken = undefined + + // Determine authentication method based on provider configuration + if (this.provider.authMode === 'oauth') { + // OAuth mode: prioritize OAuth token + try { + const oauthToken = await presenter.oauthPresenter.getAnthropicAccessToken() + if (oauthToken) { + this.oauthToken = oauthToken + this.isOAuthMode = true + console.log('[Anthropic Provider] Using OAuth token for authentication') + } else { + console.warn('[Anthropic Provider] OAuth mode selected but no OAuth token available') + } + } catch (error) { + console.log('[Anthropic Provider] Failed to get OAuth token:', error) + } + } else { + // API Key mode (default): prioritize API key + apiKey = this.provider.apiKey || process.env.ANTHROPIC_API_KEY || null + if (apiKey) { + console.log('[Anthropic Provider] Using API key for authentication') + } + } - // Get proxy configuration - const proxyUrl = proxyConfig.getProxyUrl() - const fetchOptions: { dispatcher?: ProxyAgent } = {} + // Fallback mechanism + if (!this.isOAuthMode && !apiKey) { + if (this.provider.authMode === 'oauth') { + // Fallback to API key if OAuth fails + apiKey = this.provider.apiKey || process.env.ANTHROPIC_API_KEY || null + if (apiKey) { + console.log('[Anthropic Provider] OAuth failed, falling back to API key') + } + } else { + // Fallback to OAuth if API key not available + try { + const oauthToken = await presenter.oauthPresenter.getAnthropicAccessToken() + if (oauthToken) { + this.oauthToken = oauthToken + this.isOAuthMode = true + console.log('[Anthropic Provider] API key not available, using OAuth token') + } + } catch (error) { + console.log('[Anthropic Provider] Failed to get OAuth token as fallback:', error) + } + } + } - if (proxyUrl) { - console.log(`[Anthropic Provider] Using proxy: ${proxyUrl}`) - const proxyAgent = new ProxyAgent(proxyUrl) - fetchOptions.dispatcher = proxyAgent + if (!this.isOAuthMode && !apiKey) { + console.warn('[Anthropic Provider] No API key or OAuth token available') + return + } + + // Initialize SDK only for API Key mode + if (!this.isOAuthMode && apiKey) { + // Get proxy configuration + const proxyUrl = proxyConfig.getProxyUrl() + const fetchOptions: { dispatcher?: ProxyAgent } = {} + + if (proxyUrl) { + console.log(`[Anthropic Provider] Using proxy: ${proxyUrl}`) + const proxyAgent = new ProxyAgent(proxyUrl) + fetchOptions.dispatcher = proxyAgent + } + + this.anthropic = new Anthropic({ + apiKey: apiKey, + baseURL: this.provider.baseUrl || 'https://api.anthropic.com', + defaultHeaders: this.defaultHeaders, + fetchOptions + }) } - this.anthropic = new Anthropic({ - apiKey: apiKey, - baseURL: this.provider.baseUrl || 'https://api.anthropic.com', - defaultHeaders: { - ...this.defaultHeaders - }, - fetchOptions - }) await super.init() } catch (error) { console.error('Failed to initialize Anthropic provider:', error) @@ -60,7 +117,16 @@ export class AnthropicProvider extends BaseLLMProvider { protected async fetchProviderModels(): Promise { try { - const models = await this.anthropic.models.list() + let models: any + + if (this.isOAuthMode) { + // OAuth mode: use direct HTTP request + models = await this.makeOAuthRequest('/v1/models', 'GET') + } else { + // API Key mode: use SDK + models = await this.anthropic.models.list() + } + // 引入getModelConfig函数 if (models && models.data && Array.isArray(models.data)) { const processedModels: MODEL_META[] = [] @@ -201,24 +267,80 @@ export class AnthropicProvider extends BaseLLMProvider { ] } + /** + * Make OAuth authenticated HTTP request to Anthropic API + */ + private async makeOAuthRequest(path: string, method: 'GET' | 'POST', body?: any): Promise { + if (!this.oauthToken) { + throw new Error('No OAuth token available') + } + + const baseUrl = 'https://api.anthropic.com' + const url = baseUrl + path + + const headers: Record = { + 'Content-Type': 'application/json', + 'anthropic-version': '2023-06-01', + 'anthropic-beta': 'oauth-2025-04-20', + Authorization: `Bearer ${this.oauthToken}` + } + + // Get proxy configuration + const proxyUrl = proxyConfig.getProxyUrl() + const fetchOptions: RequestInit = { + method, + headers, + ...(body && { body: JSON.stringify(body) }) + } + + if (proxyUrl) { + const proxyAgent = new ProxyAgent(proxyUrl) + // @ts-ignore - dispatcher is valid for undici-based fetch + fetchOptions.dispatcher = proxyAgent + } + + const response = await fetch(url, fetchOptions) + + if (!response.ok) { + const errorText = await response.text() + throw new Error(`OAuth API request failed: ${response.status} ${errorText}`) + } + + return await response.json() + } + public async check(): Promise<{ isOk: boolean; errorMsg: string | null }> { try { - if (!this.anthropic) { - return { isOk: false, errorMsg: '未初始化 Anthropic SDK' } - } + if (this.isOAuthMode) { + if (!this.oauthToken) { + return { isOk: false, errorMsg: 'OAuth token not available' } + } - // 发送一个简单请求来检查 API 连接状态 - await this.anthropic.messages.create({ - model: this.defaultModel, - max_tokens: 10, - messages: [{ role: 'user', content: 'Hello' }] - }) + // OAuth mode: use direct HTTP request with fixed system message + await this.makeOAuthRequest('/v1/messages', 'POST', { + model: this.defaultModel, + max_tokens: 10, + system: "You are Claude Code, Anthropic's official CLI for Claude.", + messages: [{ role: 'user', content: 'Hello' }] + }) + } else { + if (!this.anthropic) { + return { isOk: false, errorMsg: 'Anthropic SDK not initialized' } + } + + // API Key mode: use SDK + await this.anthropic.messages.create({ + model: this.defaultModel, + max_tokens: 10, + messages: [{ role: 'user', content: 'Hello' }] + }) + } return { isOk: true, errorMsg: null } } catch (error: unknown) { console.error('Anthropic API check failed:', error) const errorMessage = error instanceof Error ? error.message : String(error) - return { isOk: false, errorMsg: `API 检查失败: ${errorMessage}` } + return { isOk: false, errorMsg: `API check failed: ${errorMessage}` } } } @@ -496,6 +618,129 @@ export class AnthropicProvider extends BaseLLMProvider { } } + /** + * Format messages for OAuth mode + * Converts system messages to user messages and handles special formatting + */ + private formatMessagesForOAuth(messages: ChatMessage[]): any[] { + const result: any[] = [] + let originalSystemContent = '' + + // Extract original system message content + for (const msg of messages) { + if (msg.role === 'system') { + originalSystemContent += + (typeof msg.content === 'string' + ? msg.content + : msg.content && Array.isArray(msg.content) + ? msg.content + .filter((c) => c.type === 'text') + .map((c) => c.text || '') + .join('\n') + : '') + '\n' + } + } + + // Add original system message as first user message if exists + if (originalSystemContent.trim()) { + result.push({ + role: 'user', + content: originalSystemContent.trim() + }) + } + + // Process non-system messages + for (const msg of messages) { + if (msg.role === 'system') { + continue // Skip system messages, already converted above + } + + if (msg.role === 'user' || msg.role === 'assistant') { + const content: any[] = [] + + if (typeof msg.content === 'string') { + // Only add non-empty text content + if (msg.content.trim()) { + content.push({ + type: 'text', + text: msg.content + }) + } + } else if (Array.isArray(msg.content)) { + for (const item of msg.content) { + if (item.type === 'text') { + // Only add non-empty text content + const textContent = item.text || '' + if (textContent.trim()) { + content.push({ + type: 'text', + text: textContent + }) + } + } else if (item.type === 'image_url') { + content.push({ + type: 'image', + source: item.image_url?.url.startsWith('data:image') + ? { + type: 'base64', + data: item.image_url.url.split(',')[1], + media_type: item.image_url.url.split(';')[0].split(':')[1] as any + } + : { type: 'url', url: item.image_url?.url } + }) + } + } + } + + // Handle tool calls for assistant messages + if (msg.role === 'assistant' && 'tool_calls' in msg && Array.isArray(msg.tool_calls)) { + for (const toolCall of msg.tool_calls as any[]) { + try { + content.push({ + type: 'tool_use', + id: toolCall.id, + name: toolCall.function.name, + input: JSON.parse(toolCall.function.arguments || '{}') + }) + } catch (e) { + console.error('Error processing tool_call in OAuth mode:', e) + } + } + } + + // Only add message if it has content, otherwise add a placeholder + if (content.length === 0) { + // Add a placeholder for empty messages to prevent API errors + content.push({ + type: 'text', + text: '[Empty message]' + }) + } + + result.push({ + role: msg.role, + content: content.length === 1 && content[0].type === 'text' ? content[0].text : content + }) + } else if (msg.role === 'tool') { + // Handle tool result messages + const toolContent = + typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content) + result.push({ + role: 'user', + content: [ + { + type: 'tool_result', + tool_use_id: (msg as any).tool_call_id || '', + content: toolContent || '[Empty tool result]' + } + ] + }) + } + } + + return result + } + public async summaryTitles(messages: ChatMessage[], modelId: string): Promise { const prompt = `${SUMMARY_TITLES_PROMPT}\n\n${messages.map((m) => `${m.role}: ${m.content}`).join('\n')}` const response = await this.generateText(prompt, modelId, 0.3, 50) @@ -510,28 +755,46 @@ export class AnthropicProvider extends BaseLLMProvider { maxTokens?: number ): Promise { try { - if (!this.anthropic) { - throw new Error('Anthropic client is not initialized') - } + let requestParams: any + let response: any - const formattedMessages = this.formatMessages(messages) + if (this.isOAuthMode) { + if (!this.oauthToken) { + throw new Error('OAuth token is not available') + } - // 创建基本请求参数 - const requestParams: Anthropic.Messages.MessageCreateParams = { - model: modelId, - max_tokens: maxTokens || 1024, - temperature: temperature || 0.7, - messages: formattedMessages.messages - } + // OAuth mode: special message handling + const oauthMessages = this.formatMessagesForOAuth(messages) + requestParams = { + model: modelId, + max_tokens: maxTokens || 1024, + temperature: temperature || 0.7, + system: "You are Claude Code, Anthropic's official CLI for Claude.", + messages: oauthMessages + } - // 如果有系统消息,添加到请求参数中 - if (formattedMessages.system) { - // @ts-ignore - system 属性在类型定义中可能不存在,但API已支持 - requestParams.system = formattedMessages.system - } + response = await this.makeOAuthRequest('/v1/messages', 'POST', requestParams) + } else { + // API Key mode: use standard formatting + const formattedMessages = this.formatMessages(messages) + + requestParams = { + model: modelId, + max_tokens: maxTokens || 1024, + temperature: temperature || 0.7, + messages: formattedMessages.messages + } - // 执行请求 - const response = await this.anthropic.messages.create(requestParams) + // 如果有系统消息,添加到请求参数中 + if (formattedMessages.system) { + requestParams.system = formattedMessages.system + } + + if (!this.anthropic) { + throw new Error('Anthropic client is not initialized') + } + response = await this.anthropic.messages.create(requestParams) + } const resultResp: LLMResponse = { content: '' @@ -605,25 +868,53 @@ ${text} systemPrompt?: string ): Promise { try { - const requestParams: Anthropic.Messages.MessageCreateParams = { - model: modelId, - max_tokens: maxTokens || 1024, - temperature: temperature || 0.7, - messages: [{ role: 'user' as const, content: [{ type: 'text' as const, text: prompt }] }] - } + let requestParams: any + let response: any - // 如果提供了系统提示,添加到请求中 - if (systemPrompt) { - // @ts-ignore - system 属性在类型定义中可能不存在,但API已支持 - requestParams.system = systemPrompt - } + if (this.isOAuthMode) { + if (!this.oauthToken) { + throw new Error('OAuth token is not available') + } + + // OAuth mode: use fixed system message and prepend original system prompt to user message + let finalPrompt = prompt + if (systemPrompt) { + finalPrompt = `${systemPrompt}\n\n${prompt}` + } + + requestParams = { + model: modelId, + max_tokens: maxTokens || 1024, + temperature: temperature || 0.7, + system: "You are Claude Code, Anthropic's official CLI for Claude.", + messages: [{ role: 'user', content: finalPrompt }] + } + + response = await this.makeOAuthRequest('/v1/messages', 'POST', requestParams) + } else { + // API Key mode: use standard approach + requestParams = { + model: modelId, + max_tokens: maxTokens || 1024, + temperature: temperature || 0.7, + messages: [{ role: 'user' as const, content: [{ type: 'text' as const, text: prompt }] }] + } + + // 如果提供了系统提示,添加到请求中 + if (systemPrompt) { + requestParams.system = systemPrompt + } - const response = await this.anthropic.messages.create(requestParams) + if (!this.anthropic) { + throw new Error('Anthropic client is not initialized') + } + response = await this.anthropic.messages.create(requestParams) + } return { content: response.content - .filter((block) => block.type === 'text') - .map((block) => (block.type === 'text' ? block.text : '')) + .filter((block: any) => block.type === 'text') + .map((block: any) => (block.type === 'text' ? block.text : '')) .join(''), reasoning_content: undefined } @@ -646,28 +937,56 @@ ${text} ${context} ` try { - const requestParams: Anthropic.Messages.MessageCreateParams = { - model: modelId, - max_tokens: maxTokens || 1024, - temperature: temperature || 0.7, - messages: [{ role: 'user' as const, content: [{ type: 'text' as const, text: prompt }] }] - } + let requestParams: any + let response: any - // 如果提供了系统提示,添加到请求中 - if (systemPrompt) { - // @ts-ignore - system 属性在类型定义中可能不存在,但API已支持 - requestParams.system = systemPrompt - } + if (this.isOAuthMode) { + if (!this.oauthToken) { + throw new Error('OAuth token is not available') + } + + // OAuth mode: use fixed system message and prepend original system prompt to user message + let finalPrompt = prompt + if (systemPrompt) { + finalPrompt = `${systemPrompt}\n\n${prompt}` + } - const response = await this.anthropic.messages.create(requestParams) + requestParams = { + model: modelId, + max_tokens: maxTokens || 1024, + temperature: temperature || 0.7, + system: "You are Claude Code, Anthropic's official CLI for Claude.", + messages: [{ role: 'user', content: finalPrompt }] + } + + response = await this.makeOAuthRequest('/v1/messages', 'POST', requestParams) + } else { + // API Key mode: use standard approach + requestParams = { + model: modelId, + max_tokens: maxTokens || 1024, + temperature: temperature || 0.7, + messages: [{ role: 'user' as const, content: [{ type: 'text' as const, text: prompt }] }] + } + + // 如果提供了系统提示,添加到请求中 + if (systemPrompt) { + requestParams.system = systemPrompt + } + + if (!this.anthropic) { + throw new Error('Anthropic client is not initialized') + } + response = await this.anthropic.messages.create(requestParams) + } const suggestions = response.content - .filter((block) => block.type === 'text') - .map((block) => (block.type === 'text' ? block.text : '')) + .filter((block: any) => block.type === 'text') + .map((block: any) => (block.type === 'text' ? block.text : '')) .join('') .split('\n') - .map((s) => s.trim()) - .filter((s) => s.length > 0) + .map((s: string) => s.trim()) + .filter((s: string) => s.length > 0) .slice(0, 3) return suggestions @@ -686,9 +1005,16 @@ ${context} maxTokens: number, mcpTools: MCPToolDefinition[] ): AsyncGenerator { - if (!this.anthropic) throw new Error('Anthropic client is not initialized') if (!modelId) throw new Error('Model ID is required') console.log('modelConfig', modelConfig, modelId) + + if (this.isOAuthMode) { + // OAuth mode: use custom streaming implementation + yield* this.coreStreamOAuth(messages, modelId, modelConfig, temperature, maxTokens, mcpTools) + return + } + + if (!this.anthropic) throw new Error('Anthropic client is not initialized') try { // 格式化消息 const formattedMessagesObject = this.formatMessages(messages) @@ -945,4 +1271,201 @@ ${context} yield { type: 'stop', stop_reason: 'error' } } } + + /** + * OAuth mode streaming implementation + * Uses Server-Sent Events for streaming responses + */ + async *coreStreamOAuth( + messages: ChatMessage[], + modelId: string, + _modelConfig: ModelConfig, + temperature: number, + maxTokens: number, + mcpTools: MCPToolDefinition[] + ): AsyncGenerator { + if (!this.oauthToken) { + throw new Error('OAuth token is not available') + } + + try { + // OAuth mode: use special message formatting + const oauthMessages = this.formatMessagesForOAuth(messages) + + // 将MCP工具转换为Anthropic工具格式 + const anthropicTools = + mcpTools.length > 0 + ? await presenter.mcpPresenter.mcpToolsToAnthropicTools(mcpTools, this.provider.id) + : undefined + + // 创建基本请求参数 + const streamParams: any = { + model: modelId, + max_tokens: maxTokens || 1024, + temperature: temperature || 0.7, + system: "You are Claude Code, Anthropic's official CLI for Claude.", + messages: oauthMessages, + stream: true + } + + // 启用Claude 3.7模型的思考功能 + if (modelId.includes('claude-3-7')) { + streamParams.thinking = { budget_tokens: 1024, type: 'enabled' } + } + + // 添加工具参数 + if (anthropicTools && anthropicTools.length > 0) { + streamParams.tools = anthropicTools + } + + // Make streaming request + const baseUrl = 'https://api.anthropic.com' + const url = baseUrl + '/v1/messages' + + const headers: Record = { + 'Content-Type': 'application/json', + 'anthropic-version': '2023-06-01', + 'anthropic-beta': 'oauth-2025-04-20', + Authorization: `Bearer ${this.oauthToken}`, + Accept: 'text/event-stream' + } + + // Get proxy configuration + const proxyUrl = proxyConfig.getProxyUrl() + const fetchOptions: RequestInit = { + method: 'POST', + headers, + body: JSON.stringify(streamParams) + } + + if (proxyUrl) { + const proxyAgent = new ProxyAgent(proxyUrl) + // @ts-ignore - dispatcher is valid for undici-based fetch + fetchOptions.dispatcher = proxyAgent + } + + const response = await fetch(url, fetchOptions) + + if (!response.ok) { + const errorText = await response.text() + throw new Error(`OAuth streaming request failed: ${response.status} ${errorText}`) + } + + if (!response.body) { + throw new Error('No response body for streaming') + } + + // Parse Server-Sent Events stream + const reader = response.body.getReader() + const decoder = new TextDecoder() + let buffer = '' + let toolUseDetected = false + let currentToolId = '' + let currentToolName = '' + let accumulatedJson = '' + + try { + while (true) { + const { done, value } = await reader.read() + if (done) break + + buffer += decoder.decode(value, { stream: true }) + const lines = buffer.split('\n') + buffer = lines.pop() || '' + + for (const line of lines) { + if (line.startsWith('data: ')) { + const data = line.slice(6) + if (data === '[DONE]') continue + + try { + const chunk = JSON.parse(data) + + // Handle different event types + if ( + chunk.type === 'content_block_start' && + chunk.content_block?.type === 'tool_use' + ) { + toolUseDetected = true + currentToolId = chunk.content_block.id || `anthropic-tool-${Date.now()}` + currentToolName = chunk.content_block.name || '' + accumulatedJson = '' + + if (currentToolName) { + yield { + type: 'tool_call_start', + tool_call_id: currentToolId, + tool_call_name: currentToolName + } + } + } else if ( + chunk.type === 'content_block_delta' && + chunk.delta?.type === 'input_json_delta' + ) { + const partialJson = chunk.delta.partial_json + if (partialJson) { + accumulatedJson += partialJson + yield { + type: 'tool_call_chunk', + tool_call_id: currentToolId, + tool_call_arguments_chunk: partialJson + } + } + } else if (chunk.type === 'content_block_stop' && toolUseDetected) { + if (accumulatedJson) { + yield { + type: 'tool_call_end', + tool_call_id: currentToolId, + tool_call_arguments_complete: accumulatedJson + } + } + accumulatedJson = '' + } else if ( + chunk.type === 'content_block_delta' && + chunk.delta?.type === 'text_delta' + ) { + const text = chunk.delta.text + if (text) { + yield { + type: 'text', + content: text + } + } + } else if (chunk.type === 'message_start' && chunk.message?.usage) { + // Handle usage info if needed + } else if (chunk.type === 'message_delta' && chunk.usage) { + yield { + type: 'usage', + usage: { + prompt_tokens: chunk.usage.input_tokens || 0, + completion_tokens: chunk.usage.output_tokens || 0, + total_tokens: + (chunk.usage.input_tokens || 0) + (chunk.usage.output_tokens || 0) + } + } + } + } catch (parseError) { + console.error('Failed to parse chunk:', parseError, data) + } + } + } + } + } finally { + reader.releaseLock() + } + + // Send stop event + yield { + type: 'stop', + stop_reason: toolUseDetected ? 'tool_use' : 'complete' + } + } catch (error) { + console.error('Anthropic OAuth coreStream error:', error) + yield { + type: 'error', + error_message: error instanceof Error ? error.message : 'Unknown error' + } + yield { type: 'stop', stop_reason: 'error' } + } + } } diff --git a/src/main/presenter/oauthPresenter.ts b/src/main/presenter/oauthPresenter.ts index db710d199..eab14379a 100644 --- a/src/main/presenter/oauthPresenter.ts +++ b/src/main/presenter/oauthPresenter.ts @@ -4,6 +4,7 @@ import * as http from 'http' import { URL } from 'url' import { createGitHubCopilotOAuth } from './githubCopilotOAuth' import { createGitHubCopilotDeviceFlow } from './githubCopilotDeviceFlow' +import { createAnthropicOAuth } from './anthropicOAuth' export interface OAuthConfig { authUrl: string @@ -133,6 +134,96 @@ export class OAuthPresenter { } } + /** + * 检查Anthropic OAuth凭据是否存在 + */ + async hasAnthropicCredentials(): Promise { + try { + const anthropicOAuth = createAnthropicOAuth() + return await anthropicOAuth.hasCredentials() + } catch (error) { + console.error('Failed to check Anthropic credentials:', error) + return false + } + } + + /** + * 获取有效的Anthropic访问令牌 + */ + async getAnthropicAccessToken(): Promise { + try { + const anthropicOAuth = createAnthropicOAuth() + return await anthropicOAuth.getValidAccessToken() + } catch (error) { + console.error('Failed to get Anthropic access token:', error) + return null + } + } + + /** + * 清除Anthropic OAuth凭据 + */ + async clearAnthropicCredentials(): Promise { + try { + const anthropicOAuth = createAnthropicOAuth() + await anthropicOAuth.clearCredentials() + } catch (error) { + console.error('Failed to clear Anthropic credentials:', error) + throw error + } + } + + /** + * 启动Anthropic OAuth流程(外部浏览器) + */ + async startAnthropicOAuthFlow(): Promise { + try { + console.log('Starting Anthropic OAuth flow with external browser') + const anthropicOAuth = createAnthropicOAuth() + const authUrl = await anthropicOAuth.startOAuthFlow() + console.log('OAuth URL opened in external browser:', authUrl) + return authUrl + } catch (error) { + console.error('Failed to start Anthropic OAuth flow:', error) + throw error + } + } + + /** + * 完成Anthropic OAuth流程(使用用户输入的code) + */ + async completeAnthropicOAuthWithCode(code: string): Promise { + try { + console.log('Completing Anthropic OAuth with user-provided code') + const anthropicOAuth = createAnthropicOAuth() + const accessToken = await anthropicOAuth.completeOAuthWithCode(code) + + if (!accessToken) { + console.error('Failed to get access token from code exchange') + return false + } + + console.log('Successfully obtained access token') + return true + } catch (error) { + console.error('Failed to complete Anthropic OAuth with code:', error) + return false + } + } + + /** + * 取消Anthropic OAuth流程 + */ + async cancelAnthropicOAuthFlow(): Promise { + try { + console.log('Cancelling Anthropic OAuth flow') + const anthropicOAuth = createAnthropicOAuth() + anthropicOAuth.cancelOAuthFlow() + } catch (error) { + console.error('Failed to cancel Anthropic OAuth flow:', error) + } + } + /** * 开始OAuth登录流程(通用方法) */ diff --git a/src/renderer/src/components/settings/AnthropicProviderSettingsDetail.vue b/src/renderer/src/components/settings/AnthropicProviderSettingsDetail.vue new file mode 100644 index 000000000..4a7096177 --- /dev/null +++ b/src/renderer/src/components/settings/AnthropicProviderSettingsDetail.vue @@ -0,0 +1,682 @@ + + + diff --git a/src/renderer/src/components/settings/ModelProviderSettings.vue b/src/renderer/src/components/settings/ModelProviderSettings.vue index 7d64ae9cb..9b0c15df5 100644 --- a/src/renderer/src/components/settings/ModelProviderSettings.vue +++ b/src/renderer/src/components/settings/ModelProviderSettings.vue @@ -105,6 +105,14 @@ :provider="activeProvider" class="flex-1" /> + { setActiveProvider(provider.id) } +const handleAnthropicAuthSuccess = async () => { + // 处理 Anthropic 认证成功后的逻辑 + console.log('Anthropic auth success') + // 刷新模型列表以获取最新的授权状态 + await settingsStore.refreshAllModels() +} + +const handleAnthropicAuthError = (error: string) => { + // 处理 Anthropic 认证失败后的逻辑 + console.error('Anthropic auth error:', error) + // 可以在这里添加用户友好的错误提示 +} + // 处理拖拽结束事件 const handleDragEnd = () => { // 可以在这里添加额外的处理逻辑 diff --git a/src/renderer/src/i18n/en-US/settings.json b/src/renderer/src/i18n/en-US/settings.json index 35aaab104..48e4c3523 100644 --- a/src/renderer/src/i18n/en-US/settings.json +++ b/src/renderer/src/i18n/en-US/settings.json @@ -323,7 +323,31 @@ "toast": { "modelRunning": "The model is running", "modelRunningDesc": "Please stop the model {model} first and then delete it." - } + }, + "anthropicApiKeyTip": "Please go to Anthropic Console to get your API Key", + "anthropicConnected": "Anthropic connected", + "anthropicNotConnected": "Anthropic Not connected", + "anthropicOAuthTip": "Click Authorize DeepChat to access your Anthropic account", + "oauthLogin": "OAuth Login", + "authMethod": "Certification method", + "authMethodPlaceholder": "Select authentication method", + "apiKeyLabel": "API Key", + "apiUrlLabel": "API URL", + "anthropicOAuthFlowTip": "The system will automatically open the authorization window. Please come back and enter the authorization code after authorization.", + "anthropicBrowserOpened": "External browser is open", + "anthropicCodeInstruction": "Please complete the authorization in an external browser and paste the obtained authorization code into the input box below", + "browserOpenedSuccess": "The external browser is open, please complete the authorization", + "codeRequired": "Please enter the authorization code", + "inputOAuthCode": "Enter the authorization code", + "codeExchangeFailed": "Authorization code exchange failed", + "invalidCode": "Invalid authorization code", + "oauthCodeHint": "Please paste the authorization code here after completing the authorization in an external browser", + "oauthCodePlaceholder": "Please enter the authorization code...", + "verifyConnection": "Verify Connection", + "manageModels": "Manage Models", + "anthropicOAuthActiveTip": "OAuth authentication is enabled, you can use Anthropic services directly", + "oauthVerifySuccess": "OAuth connection verified successfully", + "oauthVerifyFailed": "OAuth connection verification failed" }, "knowledgeBase": { "title": "Knowledge Base Settings", diff --git a/src/renderer/src/i18n/fa-IR/settings.json b/src/renderer/src/i18n/fa-IR/settings.json index d6c5543e4..9f2d5b4bb 100644 --- a/src/renderer/src/i18n/fa-IR/settings.json +++ b/src/renderer/src/i18n/fa-IR/settings.json @@ -323,7 +323,31 @@ "toast": { "modelRunning": "مدل در حال اجرا است", "modelRunningDesc": "لطفاً ابتدا مدل {مدل را متوقف کرده و سپس آن را حذف کنید." - } + }, + "anthropicApiKeyTip": "لطفاً برای دریافت کلید API خود به کنسول انسان شناسی بروید", + "anthropicConnected": "انسان شناسی متصل", + "anthropicNotConnected": "انسان شناسی متصل نیست", + "anthropicOAuthTip": "برای دسترسی به حساب انسان شناسی خود ، روی مجوز DeepChat کلیک کنید", + "oauthLogin": "ورود به سیستم", + "authMethod": "روش صدور گواهینامه", + "authMethodPlaceholder": "روش احراز هویت را انتخاب کنید", + "apiKeyLabel": "کلید API", + "apiUrlLabel": "آدرس API", + "anthropicOAuthFlowTip": "سیستم به طور خودکار پنجره مجوز را باز می کند. لطفا برگردید و پس از مجوز کد مجوز را وارد کنید.", + "anthropicBrowserOpened": "مرورگر خارجی باز است", + "anthropicCodeInstruction": "لطفاً مجوز را در یک مرورگر خارجی تکمیل کرده و کد مجوز به دست آمده را در کادر ورودی زیر بچسبانید", + "browserOpenedSuccess": "مرورگر خارجی باز است ، لطفاً مجوز را تکمیل کنید", + "codeRequired": "لطفاً کد مجوز را وارد کنید", + "inputOAuthCode": "کد مجوز را وارد کنید", + "codeExchangeFailed": "مبادله کد مجوز انجام نشد", + "invalidCode": "کد مجوز نامعتبر", + "oauthCodeHint": "لطفاً پس از تکمیل مجوز در یک مرورگر خارجی ، کد مجوز را در اینجا بچسبانید", + "oauthCodePlaceholder": "لطفاً کد مجوز را وارد کنید ...", + "verifyConnection": "تأیید اتصال", + "manageModels": "مدیریت مدل‌ها", + "anthropicOAuthActiveTip": "احراز هویت OAuth فعال است، می‌توانید مستقیماً از سرویس‌های Anthropic استفاده کنید", + "oauthVerifySuccess": "اتصال OAuth با موفقیت تأیید شد", + "oauthVerifyFailed": "تأیید اتصال OAuth ناموفق بود" }, "knowledgeBase": { "title": "تنظیمات پایگاه دانش", diff --git a/src/renderer/src/i18n/fr-FR/settings.json b/src/renderer/src/i18n/fr-FR/settings.json index b2dab954a..b38c57e1a 100644 --- a/src/renderer/src/i18n/fr-FR/settings.json +++ b/src/renderer/src/i18n/fr-FR/settings.json @@ -323,7 +323,31 @@ "toast": { "modelRunning": "Le modèle est en cours d'exécution", "modelRunningDesc": "Veuillez d'abord arrêter le modèle {model}, puis le supprimer." - } + }, + "anthropicApiKeyTip": "Veuillez aller à la console anthropique pour obtenir votre clé API", + "anthropicConnected": "Connecté anthropique", + "anthropicNotConnected": "Anthropique non connecté", + "anthropicOAuthTip": "Cliquez sur Autoriser Deepchat pour accéder à votre compte anthropique", + "oauthLogin": "Connexion OAuth", + "authMethod": "Méthode de certification", + "authMethodPlaceholder": "Sélectionner la méthode d'authentification", + "apiKeyLabel": "Clé API", + "apiUrlLabel": "URL API", + "anthropicOAuthFlowTip": "Le système ouvrira automatiquement la fenêtre d'autorisation. Veuillez revenir et saisir le code d'autorisation après l'autorisation.", + "anthropicBrowserOpened": "Le navigateur externe est ouvert", + "anthropicCodeInstruction": "Veuillez compléter l'autorisation dans un navigateur externe et coller le code d'autorisation obtenu dans la zone d'entrée ci-dessous", + "browserOpenedSuccess": "Le navigateur externe est ouvert, veuillez compléter l'autorisation", + "codeRequired": "Veuillez saisir le code d'autorisation", + "inputOAuthCode": "Entrez le code d'autorisation", + "codeExchangeFailed": "Échange de code d'autorisation Échec", + "invalidCode": "Code d'autorisation non valide", + "oauthCodeHint": "Veuillez coller le code d'autorisation ici après avoir terminé l'autorisation dans un navigateur externe", + "oauthCodePlaceholder": "Veuillez saisir le code d'autorisation ...", + "verifyConnection": "Vérifier la connexion", + "manageModels": "Gérer les modèles", + "anthropicOAuthActiveTip": "L'authentification OAuth est activée, vous pouvez utiliser les services Anthropic directement", + "oauthVerifySuccess": "Connexion OAuth vérifiée avec succès", + "oauthVerifyFailed": "Échec de la vérification de la connexion OAuth" }, "knowledgeBase": { "title": "Paramètres de la base de connaissances", diff --git a/src/renderer/src/i18n/ja-JP/settings.json b/src/renderer/src/i18n/ja-JP/settings.json index 3ed1472c0..1f51097ae 100644 --- a/src/renderer/src/i18n/ja-JP/settings.json +++ b/src/renderer/src/i18n/ja-JP/settings.json @@ -323,7 +323,31 @@ "toast": { "modelRunning": "モデルが実行されています", "modelRunningDesc": "最初にモデル{model}を停止してから削除してください。" - } + }, + "anthropicApiKeyTip": "APIキーを取得するには、人類コンソールにアクセスしてください", + "anthropicConnected": "人類接続", + "anthropicNotConnected": "人類は接続されていません", + "anthropicOAuthTip": "[deepchatの承認]をクリックして、人類アカウントにアクセスします", + "oauthLogin": "oauthログイン", + "authMethod": "認証方法", + "authMethodPlaceholder": "認証方法を選択", + "apiKeyLabel": "API Key", + "apiUrlLabel": "API URL", + "anthropicOAuthFlowTip": "システムは自動的に承認ウィンドウを開きます。承認後、戻って承認コードを入力してください。", + "anthropicBrowserOpened": "外部ブラウザが開いています", + "anthropicCodeInstruction": "外部ブラウザで承認を完了し、取得した認証コードを下の入力ボックスに貼り付けてください", + "browserOpenedSuccess": "外部ブラウザが開いています。承認を完了してください", + "codeRequired": "承認コードを入力してください", + "inputOAuthCode": "承認コードを入力します", + "codeExchangeFailed": "認証コード交換は失敗しました", + "invalidCode": "無効な承認コード", + "oauthCodeHint": "外部ブラウザで承認を完了した後、ここに承認コードを貼り付けてください", + "oauthCodePlaceholder": "承認コードを入力してください...", + "verifyConnection": "接続を確認", + "manageModels": "モデル管理", + "anthropicOAuthActiveTip": "OAuth認証が有効になっており、Anthropicサービスを直接使用できます", + "oauthVerifySuccess": "OAuth接続の確認が成功しました", + "oauthVerifyFailed": "OAuth接続の確認に失敗しました" }, "knowledgeBase": { "title": "ナレッジベース設定", diff --git a/src/renderer/src/i18n/ko-KR/settings.json b/src/renderer/src/i18n/ko-KR/settings.json index b908d0b7f..ba7ecaee8 100644 --- a/src/renderer/src/i18n/ko-KR/settings.json +++ b/src/renderer/src/i18n/ko-KR/settings.json @@ -323,7 +323,31 @@ "toast": { "modelRunning": "모델이 실행 중입니다", "modelRunningDesc": "먼저 모델 {model}을 중지 한 다음 삭제하십시오." - } + }, + "anthropicApiKeyTip": "API 키를 얻으려면 Anthropic Console으로 이동하십시오.", + "anthropicConnected": "인류 연결", + "anthropicNotConnected": "인류가 연결되어 있지 않습니다", + "anthropicOAuthTip": "인류 계정에 액세스하려면 DeepChat 인증을 클릭하십시오", + "oauthLogin": "OAUTH 로그인", + "authMethod": "인증 방법", + "authMethodPlaceholder": "인증 방법 선택", + "apiKeyLabel": "API Key", + "apiUrlLabel": "API URL", + "anthropicOAuthFlowTip": "시스템은 권한 부여 창을 자동으로 엽니 다. 다시 와서 승인 후 인증 코드를 입력하십시오.", + "anthropicBrowserOpened": "외부 브라우저가 열려 있습니다", + "anthropicCodeInstruction": "외부 브라우저에서 인증을 완료하고 얻은 인증 코드를 아래 입력 상자에 붙여 넣으십시오.", + "browserOpenedSuccess": "외부 브라우저가 열려 있습니다. 승인을 완료하십시오", + "codeRequired": "인증 코드를 입력하십시오", + "inputOAuthCode": "인증 코드를 입력하십시오", + "codeExchangeFailed": "승인 코드 교환에 실패했습니다", + "invalidCode": "잘못된 권한 부여 코드", + "oauthCodeHint": "외부 브라우저에서 승인을 완료 한 후 여기에 승인 코드를 붙여 넣으십시오.", + "oauthCodePlaceholder": "승인 코드를 입력하십시오 ...", + "verifyConnection": "연결 확인", + "manageModels": "모델 관리", + "anthropicOAuthActiveTip": "OAuth 인증이 활성화되어 있어 Anthropic 서비스를 직접 사용할 수 있습니다", + "oauthVerifySuccess": "OAuth 연결 확인 성공", + "oauthVerifyFailed": "OAuth 연결 확인 실패" }, "knowledgeBase": { "title": "지식 베이스 설정", diff --git a/src/renderer/src/i18n/ru-RU/settings.json b/src/renderer/src/i18n/ru-RU/settings.json index abb819070..d1a43d137 100644 --- a/src/renderer/src/i18n/ru-RU/settings.json +++ b/src/renderer/src/i18n/ru-RU/settings.json @@ -323,7 +323,31 @@ "toast": { "modelRunning": "Модель работает", "modelRunningDesc": "Пожалуйста, остановите модель {model} сначала, а затем удалите ее." - } + }, + "anthropicApiKeyTip": "Пожалуйста, перейдите на антропную консоль, чтобы получить ключ API", + "anthropicConnected": "Антропическая связь", + "anthropicNotConnected": "Антропический не связан", + "anthropicOAuthTip": "Нажмите «Авторизировать» DeepChat для доступа к вашей антропной учетной записи", + "oauthLogin": "Оаут логин", + "authMethod": "Метод сертификации", + "authMethodPlaceholder": "Выберите метод аутентификации", + "apiKeyLabel": "API ключ", + "apiUrlLabel": "API URL", + "anthropicOAuthFlowTip": "Система автоматически откроет окно авторизации. Пожалуйста, вернитесь и введите код авторизации после авторизации.", + "anthropicBrowserOpened": "Внешний браузер открыт", + "anthropicCodeInstruction": "Пожалуйста, заполните авторизацию во внешнем браузере и вставьте полученный код авторизации в поле ввода ниже", + "browserOpenedSuccess": "Внешний браузер открыт, пожалуйста, заполните авторизацию", + "codeRequired": "Пожалуйста, введите код авторизации", + "inputOAuthCode": "Введите код авторизации", + "codeExchangeFailed": "Обмен кодом авторизации не удалось", + "invalidCode": "Неверный код авторизации", + "oauthCodeHint": "Пожалуйста, вставьте здесь код авторизации после завершения авторизации во внешнем браузере", + "oauthCodePlaceholder": "Пожалуйста, введите код авторизации ...", + "verifyConnection": "Проверить соединение", + "manageModels": "Управление моделями", + "anthropicOAuthActiveTip": "Аутентификация OAuth включена, вы можете использовать сервисы Anthropic напрямую", + "oauthVerifySuccess": "Соединение OAuth успешно проверено", + "oauthVerifyFailed": "Ошибка проверки соединения OAuth" }, "knowledgeBase": { "title": "Настройки базы знаний", diff --git a/src/renderer/src/i18n/zh-CN/settings.json b/src/renderer/src/i18n/zh-CN/settings.json index 824f0b748..ace101ac8 100644 --- a/src/renderer/src/i18n/zh-CN/settings.json +++ b/src/renderer/src/i18n/zh-CN/settings.json @@ -323,7 +323,31 @@ "keyStatus": { "usage": "已使用", "remaining": "剩余额度" - } + }, + "authMethod": "认证方式", + "authMethodPlaceholder": "选择认证方式", + "apiKeyLabel": "API Key", + "apiUrlLabel": "API URL", + "anthropicConnected": "Anthropic 已连接", + "anthropicNotConnected": "Anthropic 未连接", + "anthropicOAuthTip": "点击授权 DeepChat 访问您的 Anthropic 账户", + "oauthLogin": "OAuth 登录", + "anthropicApiKeyTip": "请前往 Anthropic Console 获取您的 API Key", + "anthropicOAuthFlowTip": "系统将自动打开授权窗口,授权后请回来输入授权码", + "browserOpenedSuccess": "外部浏览器已打开,请完成授权", + "anthropicBrowserOpened": "外部浏览器已打开", + "anthropicCodeInstruction": "请在外部浏览器中完成授权,然后将获得的授权码粘贴到下方输入框中", + "inputOAuthCode": "输入授权码", + "oauthCodeHint": "请在外部浏览器中完成授权后,将获得的授权码粘贴到此处", + "oauthCodePlaceholder": "请输入授权码...", + "codeRequired": "请输入授权码", + "invalidCode": "授权码无效", + "codeExchangeFailed": "授权码交换失败", + "verifyConnection": "验证连接", + "manageModels": "管理模型", + "anthropicOAuthActiveTip": "OAuth 认证已启用,您可以直接使用 Anthropic 服务", + "oauthVerifySuccess": "OAuth 连接验证成功", + "oauthVerifyFailed": "OAuth 连接验证失败" }, "knowledgeBase": { "title": "知识库设置", diff --git a/src/renderer/src/i18n/zh-HK/settings.json b/src/renderer/src/i18n/zh-HK/settings.json index 1de86b43c..578922045 100644 --- a/src/renderer/src/i18n/zh-HK/settings.json +++ b/src/renderer/src/i18n/zh-HK/settings.json @@ -323,7 +323,31 @@ "toast": { "modelRunning": "模型正在運行", "modelRunningDesc": "請先停止模型 {model},然後再刪除。" - } + }, + "anthropicApiKeyTip": "請前往 Anthropic Console 獲取您的 API Key", + "anthropicConnected": "Anthropic 已連接", + "anthropicNotConnected": "Anthropic 未連接", + "anthropicOAuthTip": "點擊授權 DeepChat 訪問您的 Anthropic 賬戶", + "oauthLogin": "OAuth 登錄", + "authMethod": "認證方式", + "authMethodPlaceholder": "選擇認證方式", + "apiKeyLabel": "API Key", + "apiUrlLabel": "API URL", + "anthropicOAuthFlowTip": "系統將自動打開授權窗口,授權後請回來輸入授權碼", + "anthropicBrowserOpened": "外部瀏覽器已打開", + "anthropicCodeInstruction": "請在外部瀏覽器中完成授權,然後將獲得的授權碼粘貼到下方輸入框中", + "browserOpenedSuccess": "外部瀏覽器已打開,請完成授權", + "codeRequired": "請輸入授權碼", + "inputOAuthCode": "輸入授權碼", + "codeExchangeFailed": "授權碼交換失敗", + "invalidCode": "授權碼無效", + "oauthCodeHint": "請在外部瀏覽器中完成授權後,將獲得的授權碼粘貼到此處", + "oauthCodePlaceholder": "請輸入授權碼...", + "verifyConnection": "驗證連接", + "manageModels": "管理模型", + "anthropicOAuthActiveTip": "OAuth 認證已啟用,您可以直接使用 Anthropic 服務", + "oauthVerifySuccess": "OAuth 連接驗證成功", + "oauthVerifyFailed": "OAuth 連接驗證失敗" }, "knowledgeBase": { "fastgptTitle": "FastGPT知識庫", diff --git a/src/renderer/src/i18n/zh-TW/settings.json b/src/renderer/src/i18n/zh-TW/settings.json index c61e6646a..6937b6072 100644 --- a/src/renderer/src/i18n/zh-TW/settings.json +++ b/src/renderer/src/i18n/zh-TW/settings.json @@ -323,7 +323,31 @@ "toast": { "modelRunning": "模型正在運行", "modelRunningDesc": "請先停止模型 {model},然後再刪除。" - } + }, + "anthropicApiKeyTip": "請前往 Anthropic Console 獲取您的 API Key", + "anthropicConnected": "Anthropic 已連接", + "anthropicNotConnected": "Anthropic 未連接", + "anthropicOAuthTip": "點擊授權 DeepChat 訪問您的 Anthropic 賬戶", + "oauthLogin": "OAuth 登錄", + "authMethod": "認證方式", + "authMethodPlaceholder": "選擇認證方式", + "apiKeyLabel": "API Key", + "apiUrlLabel": "API URL", + "anthropicOAuthFlowTip": "系統將自動打開授權窗口,授權後請回來輸入授權碼", + "anthropicBrowserOpened": "外部瀏覽器已打開", + "anthropicCodeInstruction": "請在外部瀏覽器中完成授權,然後將獲得的授權碼粘貼到下方輸入框中", + "browserOpenedSuccess": "外部瀏覽器已打開,請完成授權", + "codeRequired": "請輸入授權碼", + "inputOAuthCode": "輸入授權碼", + "codeExchangeFailed": "授權碼交換失敗", + "invalidCode": "授權碼無效", + "oauthCodeHint": "請在外部瀏覽器中完成授權後,將獲得的授權碼粘貼到此處", + "oauthCodePlaceholder": "請輸入授權碼...", + "verifyConnection": "驗證連接", + "manageModels": "管理模型", + "anthropicOAuthActiveTip": "OAuth 認證已啟用,您可以直接使用 Anthropic 服務", + "oauthVerifySuccess": "OAuth 連接驗證成功", + "oauthVerifyFailed": "OAuth 連接驗證失敗" }, "knowledgeBase": { "title": "知識庫設置", diff --git a/src/renderer/src/stores/settings.ts b/src/renderer/src/stores/settings.ts index 7d4062d93..771861890 100644 --- a/src/renderer/src/stores/settings.ts +++ b/src/renderer/src/stores/settings.ts @@ -609,7 +609,7 @@ export const useSettingsStore = defineStore('settings', () => { // 使用 throttle 包装的刷新函数,确保在频繁调用时最后一次调用能够成功执行 // trailing: true 确保在节流周期结束后执行最后一次调用 // leading: false 避免立即执行第一次调用 - const refreshAllModels = useThrottleFn(_refreshAllModelsInternal, 1000, true, false) + const refreshAllModels = useThrottleFn(_refreshAllModelsInternal, 1000, true, true) // 搜索模型 const searchModels = (query: string) => { @@ -871,6 +871,18 @@ export const useSettingsStore = defineStore('settings', () => { await updateProviderConfig(providerId, updates) } + // 更新provider的认证配置 + const updateProviderAuth = async ( + providerId: string, + authMode?: 'apikey' | 'oauth', + oauthToken?: string + ): Promise => { + const updates: Partial = {} + if (authMode !== undefined) updates.authMode = authMode + if (oauthToken !== undefined) updates.oauthToken = oauthToken + await updateProviderConfig(providerId, updates) + } + // 更新provider的启用状态 const updateProviderStatus = async (providerId: string, enable: boolean): Promise => { // 更新时间戳 @@ -1540,6 +1552,7 @@ export const useSettingsStore = defineStore('settings', () => { updateCustomModel, updateProviderConfig, updateProviderApi, + updateProviderAuth, updateProviderStatus, refreshProviderModels, setSearchEngine, diff --git a/src/shared/presenter.d.ts b/src/shared/presenter.d.ts index 9aedaf90f..e4fcb333c 100644 --- a/src/shared/presenter.d.ts +++ b/src/shared/presenter.d.ts @@ -288,6 +288,12 @@ export interface IOAuthPresenter { startOAuthLogin(providerId: string, config: OAuthConfig): Promise startGitHubCopilotLogin(providerId: string): Promise startGitHubCopilotDeviceFlowLogin(providerId: string): Promise + startAnthropicOAuthFlow(): Promise + completeAnthropicOAuthWithCode(code: string): Promise + cancelAnthropicOAuthFlow(): Promise + hasAnthropicCredentials(): Promise + getAnthropicAccessToken(): Promise + clearAnthropicCredentials(): Promise } export interface OAuthConfig { @@ -476,6 +482,8 @@ export type LLM_PROVIDER = { baseUrl: string enable: boolean custom?: boolean + authMode?: 'apikey' | 'oauth' // 认证模式 + oauthToken?: string // OAuth token websites?: { official: string apiKey: string