Commit 103b8cddbab0df4a74b096f2239b16c9d0a40c29

Authored by yibo
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 请求失败。
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 }