From 6ec4f4bf5b55265f60c9de153244730bc6209a05 Mon Sep 17 00:00:00 2001 From: shaw Date: Sat, 29 Nov 2025 14:13:28 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8Dclaude=20console?= =?UTF-8?q?=E8=B4=A6=E5=8F=B7Test=E6=9C=AA=E5=93=8D=E5=BA=94=E7=9A=84?= =?UTF-8?q?=E7=9A=84bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/services/claudeConsoleRelayService.js | 219 +++++++++++++++++++--- 1 file changed, 191 insertions(+), 28 deletions(-) diff --git a/src/services/claudeConsoleRelayService.js b/src/services/claudeConsoleRelayService.js index aa883242..5f01cfc2 100644 --- a/src/services/claudeConsoleRelayService.js +++ b/src/services/claudeConsoleRelayService.js @@ -12,7 +12,7 @@ const { class ClaudeConsoleRelayService { constructor() { - this.defaultUserAgent = 'claude-cli/1.0.69 (external, cli)' + this.defaultUserAgent = 'claude-cli/2.0.52 (external, cli)' } // 🚀 转发请求到Claude Console API @@ -1113,20 +1113,53 @@ class ClaudeConsoleRelayService { } } - // 🧪 测试账号连接(供Admin API使用,直接复用 _makeClaudeConsoleStreamRequest) + // 🧪 测试账号连接(供Admin API使用,独立处理以确保错误时也返回SSE格式) async testAccountConnection(accountId, responseStream) { const testRequestBody = { model: 'claude-sonnet-4-5-20250929', - max_tokens: 100, + max_tokens: 32000, stream: true, messages: [ { role: 'user', content: 'hi' } + ], + system: [ + { + type: 'text', + text: "You are Claude Code, Anthropic's official CLI for Claude." + } ] } + // 辅助函数:发送 SSE 事件 + const sendSSEEvent = (type, data) => { + if (!responseStream.destroyed && !responseStream.writableEnded) { + try { + responseStream.write(`data: ${JSON.stringify({ type, ...data })}\n\n`) + } catch { + // 忽略写入错误 + } + } + } + + // 辅助函数:结束测试并关闭流 + const endTest = (success, error = null) => { + if (!responseStream.destroyed && !responseStream.writableEnded) { + try { + if (success) { + sendSSEEvent('test_complete', { success: true }) + } else { + sendSSEEvent('test_complete', { success: false, error: error || '测试失败' }) + } + responseStream.end() + } catch { + // 忽略写入错误 + } + } + } + try { // 获取账户信息 const account = await claudeConsoleAccountService.getAccount(accountId) @@ -1149,35 +1182,165 @@ class ClaudeConsoleRelayService { }) } - // 创建流转换器,将 Claude API 格式转换为前端测试页面期望的格式 - const streamTransformer = this._createTestStreamTransformer() + // 发送测试开始事件 + sendSSEEvent('test_start', {}) - // 直接复用现有的流式请求方法 - await this._makeClaudeConsoleStreamRequest( - testRequestBody, - account, - proxyAgent, - {}, // clientHeaders - 测试不需要客户端headers - responseStream, - accountId, - null, // usageCallback - 测试不需要统计 - streamTransformer, // 使用转换器将 Claude API 格式转为前端期望格式 - {} // requestOptions - ) + // 构建完整的API URL + const cleanUrl = account.apiUrl.replace(/\/$/, '') + const apiEndpoint = cleanUrl.endsWith('/v1/messages') ? cleanUrl : `${cleanUrl}/v1/messages` - logger.info(`✅ Test request completed for account: ${account.name}`) + // 决定使用的 User-Agent + const userAgent = account.userAgent || this.defaultUserAgent + + // 准备请求配置 + const requestConfig = { + method: 'POST', + url: apiEndpoint, + data: testRequestBody, + headers: { + 'Content-Type': 'application/json', + 'anthropic-version': '2023-06-01', + 'User-Agent': userAgent + }, + timeout: 30000, // 测试请求使用较短超时 + responseType: 'stream', + validateStatus: () => true + } + + if (proxyAgent) { + requestConfig.httpAgent = proxyAgent + requestConfig.httpsAgent = proxyAgent + requestConfig.proxy = false + } + + // 设置认证方式 + if (account.apiKey && account.apiKey.startsWith('sk-ant-')) { + requestConfig.headers['x-api-key'] = account.apiKey + } else { + requestConfig.headers['Authorization'] = `Bearer ${account.apiKey}` + } + + // 发送请求 + const response = await axios(requestConfig) + + logger.debug(`🌊 Claude Console test response status: ${response.status}`) + + // 处理非200响应 + if (response.status !== 200) { + logger.error( + `❌ Claude Console API returned error status: ${response.status} | Account: ${account?.name || accountId}` + ) + + // 收集错误响应数据 + return new Promise((resolve) => { + const errorChunks = [] + + response.data.on('data', (chunk) => { + errorChunks.push(chunk) + }) + + response.data.on('end', () => { + try { + const fullErrorData = Buffer.concat(errorChunks).toString() + logger.error( + `📝 [Test] Upstream error response from ${account?.name || accountId}: ${fullErrorData.substring(0, 500)}` + ) + + // 尝试解析错误信息 + let errorMessage = `API Error: ${response.status}` + try { + const errorJson = JSON.parse(fullErrorData) + // 直接提取所有可能的错误信息字段 + errorMessage = + errorJson.message || + errorJson.error?.message || + errorJson.statusMessage || + errorJson.error || + (typeof errorJson === 'string' ? errorJson : JSON.stringify(errorJson)) + } catch { + errorMessage = fullErrorData.substring(0, 200) || `API Error: ${response.status}` + } + + endTest(false, errorMessage) + resolve() + } catch { + endTest(false, `API Error: ${response.status}`) + resolve() + } + }) + + response.data.on('error', (err) => { + endTest(false, err.message || '流读取错误') + resolve() + }) + }) + } + + // 处理成功的流式响应 + return new Promise((resolve) => { + let buffer = '' + + response.data.on('data', (chunk) => { + try { + buffer += chunk.toString() + const lines = buffer.split('\n') + buffer = lines.pop() || '' + + for (const line of lines) { + if (!line.startsWith('data: ')) { + continue + } + + const jsonStr = line.substring(6).trim() + if (!jsonStr || jsonStr === '[DONE]') { + continue + } + + try { + const data = JSON.parse(jsonStr) + + // 转换 content_block_delta 为 content + if (data.type === 'content_block_delta' && data.delta && data.delta.text) { + sendSSEEvent('content', { text: data.delta.text }) + } + + // 处理消息完成 + if (data.type === 'message_stop') { + endTest(true) + } + + // 处理错误事件 + if (data.type === 'error') { + const errorMsg = data.error?.message || data.message || '未知错误' + endTest(false, errorMsg) + } + } catch { + // 忽略解析错误 + } + } + } catch { + // 忽略处理错误 + } + }) + + response.data.on('end', () => { + logger.info(`✅ Test request completed for account: ${account.name}`) + // 如果还没结束,发送完成事件 + if (!responseStream.destroyed && !responseStream.writableEnded) { + endTest(true) + } + resolve() + }) + + response.data.on('error', (err) => { + logger.error(`❌ Test stream error:`, err) + endTest(false, err.message || '流处理错误') + resolve() + }) + }) } catch (error) { logger.error(`❌ Test account connection failed:`, error) - // 发送错误事件给前端 - if (!responseStream.destroyed && !responseStream.writableEnded) { - try { - const errorMsg = error.message || '测试失败' - responseStream.write(`data: ${JSON.stringify({ type: 'error', error: errorMsg })}\n\n`) - } catch { - // 忽略写入错误 - } - } - throw error + endTest(false, error.message || '测试失败') } }