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 | 81 | |
| 82 | 82 | // stdout → NDJSON 解析 |
| 83 | 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 | 87 | const lines = this._buffer.split('\n'); |
| 86 | 88 | this._buffer = lines.pop() || ''; |
| 87 | 89 | |
| ... | ... | @@ -103,7 +105,7 @@ class ClaudeCliProtocol extends EventEmitter { |
| 103 | 105 | this._child.stderr.on('data', (data) => { |
| 104 | 106 | const msg = data.toString().trim(); |
| 105 | 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 | 109 | this.emit('stderr', msg); |
| 108 | 110 | |
| 109 | 111 | // 检测致命错误:如果 sendPrompt 正在等待,主动 reject 防止 Promise 永远挂起 |
| ... | ... | @@ -252,6 +254,7 @@ class ClaudeCliProtocol extends EventEmitter { |
| 252 | 254 | * @private |
| 253 | 255 | */ |
| 254 | 256 | _handleStreamEvent(event) { |
| 257 | + console.log(`[ClaudeCliProtocol] 🔄 event.type=${event.type}`); | |
| 255 | 258 | switch (event.type) { |
| 256 | 259 | case 'system': |
| 257 | 260 | this.sessionId = event.session_id || this.sessionId; |
| ... | ... | @@ -302,6 +305,26 @@ class ClaudeCliProtocol extends EventEmitter { |
| 302 | 305 | this._promptReject = null; |
| 303 | 306 | } |
| 304 | 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 | 329 | default: |
| 307 | 330 | this.emit('acp_event', { | ... | ... |
server/routes/agent-chat.js
| ... | ... | @@ -1664,6 +1664,7 @@ router.post('/:agentId/chat', async (req, res) => { |
| 1664 | 1664 | const onUpdate = (payload) => { |
| 1665 | 1665 | const update = payload.update; |
| 1666 | 1666 | if (update?.sessionUpdate === 'agent_message_chunk' && update.content?.text) { |
| 1667 | + console.log(`[SSE-DEBUG] 📤 agent_output (${update.content.text.length} chars)`); | |
| 1667 | 1668 | fullOutput += update.content.text; |
| 1668 | 1669 | logMessageChunk(agentId, convId, update.content.text); |
| 1669 | 1670 | sendSSE('agent_output', { agent: agentId, message: update.content.text }); |
| ... | ... | @@ -1751,6 +1752,9 @@ router.post('/:agentId/chat', async (req, res) => { |
| 1751 | 1752 | entry.protocol.on('stderr', onStderr); |
| 1752 | 1753 | entry.protocol.on('disconnected', onDisconnected); |
| 1753 | 1754 | |
| 1755 | + // 发出状态以便前端展示不卡壳 | |
| 1756 | + sendSSE('agent_output', { agent: agentId, message: `_[系统正在通过 \`${fullSystemConfig.model}\` 请求响应...]_\\\n\\\n` }); | |
| 1757 | + | |
| 1754 | 1758 | // 发送 prompt 并等待完成 |
| 1755 | 1759 | await entry.protocol.sendPrompt(prompt); |
| 1756 | 1760 | |
| ... | ... | @@ -1895,6 +1899,7 @@ ${resultsText.join('\n\n')} |
| 1895 | 1899 | sessions.delete(key); |
| 1896 | 1900 | |
| 1897 | 1901 | sendSSE('error', { message: err.message }); |
| 1902 | + console.log(`[SSE-DEBUG] 🚨 sendSSE('error') called:`, err.message.substring(0, 200)); | |
| 1898 | 1903 | } finally { |
| 1899 | 1904 | res.end(); |
| 1900 | 1905 | } | ... | ... |