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 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 }
... ...