mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-22 16:43:35 +00:00
fix: 修复claude console账号Test未响应的的bug
This commit is contained in:
@@ -12,7 +12,7 @@ const {
|
|||||||
|
|
||||||
class ClaudeConsoleRelayService {
|
class ClaudeConsoleRelayService {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.defaultUserAgent = 'claude-cli/1.0.69 (external, cli)'
|
this.defaultUserAgent = 'claude-cli/2.0.52 (external, cli)'
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🚀 转发请求到Claude Console API
|
// 🚀 转发请求到Claude Console API
|
||||||
@@ -1113,20 +1113,53 @@ class ClaudeConsoleRelayService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🧪 测试账号连接(供Admin API使用,直接复用 _makeClaudeConsoleStreamRequest)
|
// 🧪 测试账号连接(供Admin API使用,独立处理以确保错误时也返回SSE格式)
|
||||||
async testAccountConnection(accountId, responseStream) {
|
async testAccountConnection(accountId, responseStream) {
|
||||||
const testRequestBody = {
|
const testRequestBody = {
|
||||||
model: 'claude-sonnet-4-5-20250929',
|
model: 'claude-sonnet-4-5-20250929',
|
||||||
max_tokens: 100,
|
max_tokens: 32000,
|
||||||
stream: true,
|
stream: true,
|
||||||
messages: [
|
messages: [
|
||||||
{
|
{
|
||||||
role: 'user',
|
role: 'user',
|
||||||
content: 'hi'
|
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 {
|
try {
|
||||||
// 获取账户信息
|
// 获取账户信息
|
||||||
const account = await claudeConsoleAccountService.getAccount(accountId)
|
const account = await claudeConsoleAccountService.getAccount(accountId)
|
||||||
@@ -1149,35 +1182,165 @@ class ClaudeConsoleRelayService {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建流转换器,将 Claude API 格式转换为前端测试页面期望的格式
|
// 发送测试开始事件
|
||||||
const streamTransformer = this._createTestStreamTransformer()
|
sendSSEEvent('test_start', {})
|
||||||
|
|
||||||
// 直接复用现有的流式请求方法
|
// 构建完整的API URL
|
||||||
await this._makeClaudeConsoleStreamRequest(
|
const cleanUrl = account.apiUrl.replace(/\/$/, '')
|
||||||
testRequestBody,
|
const apiEndpoint = cleanUrl.endsWith('/v1/messages') ? cleanUrl : `${cleanUrl}/v1/messages`
|
||||||
account,
|
|
||||||
proxyAgent,
|
|
||||||
{}, // clientHeaders - 测试不需要客户端headers
|
|
||||||
responseStream,
|
|
||||||
accountId,
|
|
||||||
null, // usageCallback - 测试不需要统计
|
|
||||||
streamTransformer, // 使用转换器将 Claude API 格式转为前端期望格式
|
|
||||||
{} // requestOptions
|
|
||||||
)
|
|
||||||
|
|
||||||
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) {
|
} catch (error) {
|
||||||
logger.error(`❌ Test account connection failed:`, error)
|
logger.error(`❌ Test account connection failed:`, error)
|
||||||
// 发送错误事件给前端
|
endTest(false, error.message || '测试失败')
|
||||||
if (!responseStream.destroyed && !responseStream.writableEnded) {
|
|
||||||
try {
|
|
||||||
const errorMsg = error.message || '测试失败'
|
|
||||||
responseStream.write(`data: ${JSON.stringify({ type: 'error', error: errorMsg })}\n\n`)
|
|
||||||
} catch {
|
|
||||||
// 忽略写入错误
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw error
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user