Commit 103b8cddbab0df4a74b096f2239b16c9d0a40c29
1 parent
bb089e3f
fix: 拦截和阻断 Claude CLI 流式输出中的 API Error (如 Http 429)
- 当底层模型 (MiniMax等) 抛出 API 错误时,Claude CLI 不会崩溃也不会输出 stderr,而是通过 stdout 输出 event.type='error' 的 JSON 块。 - 未处理该事件导致协议层无法等到 result 事件,引发 Promise 永久挂起。 - 修复:在_handleStreamEvent 拦截 error 块,主动抛出错误打断等待链,前端即可正常展示 ❌ API 请求失败。
Showing
2 changed files
with
30 additions
and
2 deletions
packages/acp-core/protocol/ClaudeCliProtocol.js
| @@ -81,7 +81,9 @@ class ClaudeCliProtocol extends EventEmitter { | @@ -81,7 +81,9 @@ class ClaudeCliProtocol extends EventEmitter { | ||
| 81 | 81 | ||
| 82 | // stdout → NDJSON 解析 | 82 | // stdout → NDJSON 解析 |
| 83 | this._child.stdout.on('data', (data) => { | 83 | this._child.stdout.on('data', (data) => { |
| 84 | - this._buffer += data.toString(); | 84 | + const raw = data.toString(); |
| 85 | + console.log(`[ClaudeCliProtocol] 📥 stdout (${raw.length} bytes):`, raw.substring(0, 200)); | ||
| 86 | + this._buffer += raw; | ||
| 85 | const lines = this._buffer.split('\n'); | 87 | const lines = this._buffer.split('\n'); |
| 86 | this._buffer = lines.pop() || ''; | 88 | this._buffer = lines.pop() || ''; |
| 87 | 89 | ||
| @@ -103,7 +105,7 @@ class ClaudeCliProtocol extends EventEmitter { | @@ -103,7 +105,7 @@ class ClaudeCliProtocol extends EventEmitter { | ||
| 103 | this._child.stderr.on('data', (data) => { | 105 | this._child.stderr.on('data', (data) => { |
| 104 | const msg = data.toString().trim(); | 106 | const msg = data.toString().trim(); |
| 105 | if (msg) { | 107 | if (msg) { |
| 106 | - console.log('[ClaudeCliProtocol] stderr:', msg.substring(0, 300)); | 108 | + console.log(`[ClaudeCliProtocol] 🚨 stderr (${msg.length} bytes):`, msg.substring(0, 500)); |
| 107 | this.emit('stderr', msg); | 109 | this.emit('stderr', msg); |
| 108 | 110 | ||
| 109 | // 检测致命错误:如果 sendPrompt 正在等待,主动 reject 防止 Promise 永远挂起 | 111 | // 检测致命错误:如果 sendPrompt 正在等待,主动 reject 防止 Promise 永远挂起 |
| @@ -252,6 +254,7 @@ class ClaudeCliProtocol extends EventEmitter { | @@ -252,6 +254,7 @@ class ClaudeCliProtocol extends EventEmitter { | ||
| 252 | * @private | 254 | * @private |
| 253 | */ | 255 | */ |
| 254 | _handleStreamEvent(event) { | 256 | _handleStreamEvent(event) { |
| 257 | + console.log(`[ClaudeCliProtocol] 🔄 event.type=${event.type}`); | ||
| 255 | switch (event.type) { | 258 | switch (event.type) { |
| 256 | case 'system': | 259 | case 'system': |
| 257 | this.sessionId = event.session_id || this.sessionId; | 260 | this.sessionId = event.session_id || this.sessionId; |
| @@ -302,6 +305,26 @@ class ClaudeCliProtocol extends EventEmitter { | @@ -302,6 +305,26 @@ class ClaudeCliProtocol extends EventEmitter { | ||
| 302 | this._promptReject = null; | 305 | this._promptReject = null; |
| 303 | } | 306 | } |
| 304 | break; | 307 | break; |
| 308 | + case 'error': | ||
| 309 | + this.emit('acp_event', { | ||
| 310 | + type: 'notification', | ||
| 311 | + method: 'claude/error', | ||
| 312 | + params: event, | ||
| 313 | + timestamp: Date.now(), | ||
| 314 | + }); | ||
| 315 | + | ||
| 316 | + // API 报错(如 MiniMax 429)会通过 stdout 下发 error 对象,此时不会下发 result | ||
| 317 | + if (this._promptReject) { | ||
| 318 | + const errMsg = event.error?.message || event.message || JSON.stringify(event); | ||
| 319 | + const err = new Error(`API 请求失败: ${errMsg}`); | ||
| 320 | + console.error(`[ClaudeCliProtocol] 拦截到底层 API 错误:`, err.message); | ||
| 321 | + | ||
| 322 | + if (this._idleTimer) { clearTimeout(this._idleTimer); this._idleTimer = null; } | ||
| 323 | + this._promptReject(err); | ||
| 324 | + this._promptResolve = null; | ||
| 325 | + this._promptReject = null; | ||
| 326 | + } | ||
| 327 | + break; | ||
| 305 | 328 | ||
| 306 | default: | 329 | default: |
| 307 | this.emit('acp_event', { | 330 | this.emit('acp_event', { |
server/routes/agent-chat.js
| @@ -1664,6 +1664,7 @@ router.post('/:agentId/chat', async (req, res) => { | @@ -1664,6 +1664,7 @@ router.post('/:agentId/chat', async (req, res) => { | ||
| 1664 | const onUpdate = (payload) => { | 1664 | const onUpdate = (payload) => { |
| 1665 | const update = payload.update; | 1665 | const update = payload.update; |
| 1666 | if (update?.sessionUpdate === 'agent_message_chunk' && update.content?.text) { | 1666 | if (update?.sessionUpdate === 'agent_message_chunk' && update.content?.text) { |
| 1667 | + console.log(`[SSE-DEBUG] 📤 agent_output (${update.content.text.length} chars)`); | ||
| 1667 | fullOutput += update.content.text; | 1668 | fullOutput += update.content.text; |
| 1668 | logMessageChunk(agentId, convId, update.content.text); | 1669 | logMessageChunk(agentId, convId, update.content.text); |
| 1669 | sendSSE('agent_output', { agent: agentId, message: update.content.text }); | 1670 | sendSSE('agent_output', { agent: agentId, message: update.content.text }); |
| @@ -1751,6 +1752,9 @@ router.post('/:agentId/chat', async (req, res) => { | @@ -1751,6 +1752,9 @@ router.post('/:agentId/chat', async (req, res) => { | ||
| 1751 | entry.protocol.on('stderr', onStderr); | 1752 | entry.protocol.on('stderr', onStderr); |
| 1752 | entry.protocol.on('disconnected', onDisconnected); | 1753 | entry.protocol.on('disconnected', onDisconnected); |
| 1753 | 1754 | ||
| 1755 | + // 发出状态以便前端展示不卡壳 | ||
| 1756 | + sendSSE('agent_output', { agent: agentId, message: `_[系统正在通过 \`${fullSystemConfig.model}\` 请求响应...]_\\\n\\\n` }); | ||
| 1757 | + | ||
| 1754 | // 发送 prompt 并等待完成 | 1758 | // 发送 prompt 并等待完成 |
| 1755 | await entry.protocol.sendPrompt(prompt); | 1759 | await entry.protocol.sendPrompt(prompt); |
| 1756 | 1760 | ||
| @@ -1895,6 +1899,7 @@ ${resultsText.join('\n\n')} | @@ -1895,6 +1899,7 @@ ${resultsText.join('\n\n')} | ||
| 1895 | sessions.delete(key); | 1899 | sessions.delete(key); |
| 1896 | 1900 | ||
| 1897 | sendSSE('error', { message: err.message }); | 1901 | sendSSE('error', { message: err.message }); |
| 1902 | + console.log(`[SSE-DEBUG] 🚨 sendSSE('error') called:`, err.message.substring(0, 200)); | ||
| 1898 | } finally { | 1903 | } finally { |
| 1899 | res.end(); | 1904 | res.end(); |
| 1900 | } | 1905 | } |