Commit 86f37a4be714c331d3c52e4514fbec023f6312bf
1 parent
4a688046
fix: Claude CLI 错误反馈到前端 + 致命错误自动中断
- ClaudeCliProtocol: stderr 检测 rate_limit/429/401 等致命错误时自动 reject sendPrompt Promise - agent-chat.js: 监听 stderr/disconnected 事件并通过 SSE 推送到前端 - AgentChat.vue: error 事件停止 thinking 状态 + ACP 日志面板显示错误
Showing
5 changed files
with
49 additions
and
2 deletions
data/goals/_index.json
0 → 100644
data/system/agents/master/memory/short-term/session-1774547297290.md
0 → 100644
| 1 | +# Session 摘要 | |
| 2 | + | |
| 3 | +- **时间**: 2026-03-26 17:48:17 | |
| 4 | +- **对话ID**: conv-mn7rjae6 | |
| 5 | +- **状态**: completed | |
| 6 | +- **工具调用**: 0 次 | |
| 7 | +- **耗时**: 3分钟 | |
| 8 | + | |
| 9 | +## 任务 | |
| 10 | +我需要创建一个名为 ABC 的 AI 互动剧公司。这家公司的核心业务逻辑和组织架构诉求如下: | |
| 11 | + | |
| 12 | +一、 | |
| 13 | + | |
| 14 | +## 结果 | |
| 15 | +API Error: 429 {"type":"error","error":{"type":"rate_limit_error","message":"usage limit exceeded (2056)"},"request_id":"0614a461f8ade83e80e74a117efb3ac7"} | ... | ... |
packages/acp-core/protocol/ClaudeCliProtocol.js
| ... | ... | @@ -96,12 +96,21 @@ class ClaudeCliProtocol extends EventEmitter { |
| 96 | 96 | } |
| 97 | 97 | }); |
| 98 | 98 | |
| 99 | - // stderr 转发 | |
| 99 | + // stderr 转发 + 致命错误检测 | |
| 100 | 100 | this._child.stderr.on('data', (data) => { |
| 101 | 101 | const msg = data.toString().trim(); |
| 102 | 102 | if (msg) { |
| 103 | 103 | console.log('[ClaudeCliProtocol] stderr:', msg.substring(0, 300)); |
| 104 | 104 | this.emit('stderr', msg); |
| 105 | + | |
| 106 | + // 检测致命错误:如果 sendPrompt 正在等待,主动 reject 防止 Promise 永远挂起 | |
| 107 | + if (this._promptReject && /rate.?limit|usage.?limit|429|401|403|authentication|api.?key|quota|billing/i.test(msg)) { | |
| 108 | + const err = new Error(`Claude CLI 致命错误: ${msg.substring(0, 500)}`); | |
| 109 | + this._promptReject(err); | |
| 110 | + this._promptResolve = null; | |
| 111 | + this._promptReject = null; | |
| 112 | + console.error('[ClaudeCliProtocol] 致命错误已触发 Promise reject'); | |
| 113 | + } | |
| 105 | 114 | } |
| 106 | 115 | }); |
| 107 | 116 | ... | ... |
server/routes/agent-chat.js
| ... | ... | @@ -1734,12 +1734,31 @@ router.post('/:agentId/chat', async (req, res) => { |
| 1734 | 1734 | entry.protocol.on('session_update', onUpdate); |
| 1735 | 1735 | entry.protocol.on('acp_event', onAcpEvent); |
| 1736 | 1736 | |
| 1737 | + // 监听 stderr / error / disconnect — 将 Claude CLI 错误实时推送到前端 | |
| 1738 | + const onStderr = (msg) => { | |
| 1739 | + console.warn(`[AgentChat] stderr from ${agentId}:`, msg.substring(0, 300)); | |
| 1740 | + sendSSE('acp_raw', { method: 'claude/stderr', params: { message: msg }, ts: Date.now() }); | |
| 1741 | + // 如果包含严重错误关键词,额外发一条 error 事件 | |
| 1742 | + if (/error|rate.?limit|429|403|timeout|ECONNREFUSED|failed/i.test(msg)) { | |
| 1743 | + sendSSE('error', { message: `⚠️ Claude CLI: ${msg.substring(0, 500)}` }); | |
| 1744 | + } | |
| 1745 | + }; | |
| 1746 | + const onDisconnected = ({ code, signal }) => { | |
| 1747 | + if (code !== 0) { | |
| 1748 | + sendSSE('error', { message: `Claude CLI 异常退出 (code=${code}, signal=${signal})` }); | |
| 1749 | + } | |
| 1750 | + }; | |
| 1751 | + entry.protocol.on('stderr', onStderr); | |
| 1752 | + entry.protocol.on('disconnected', onDisconnected); | |
| 1753 | + | |
| 1737 | 1754 | // 发送 prompt 并等待完成 |
| 1738 | 1755 | await entry.protocol.sendPrompt(prompt); |
| 1739 | 1756 | |
| 1740 | 1757 | // 清理监听 |
| 1741 | 1758 | entry.protocol.off('session_update', onUpdate); |
| 1742 | 1759 | entry.protocol.off('acp_event', onAcpEvent); |
| 1760 | + entry.protocol.off('stderr', onStderr); | |
| 1761 | + entry.protocol.off('disconnected', onDisconnected); | |
| 1743 | 1762 | |
| 1744 | 1763 | // 持久化当前轮次的回复 |
| 1745 | 1764 | if (fullOutput) { | ... | ... |
web/src/views/AgentChat.vue
| ... | ... | @@ -564,7 +564,10 @@ function handleEvent(type, data) { |
| 564 | 564 | else if (data.event === 'start') subAgents.value[data.callId].status = 'running' |
| 565 | 565 | scrollToBottom(); break |
| 566 | 566 | case 'error': |
| 567 | - messages.value.push({ role: 'assistant', content: `❌ ${data.message}`, timestamp: new Date().toISOString() }); break | |
| 567 | + messages.value.push({ role: 'assistant', content: `❌ ${data.message}`, timestamp: new Date().toISOString() }) | |
| 568 | + acpLog.value.push({ type: 'error', icon: '🚨', text: data.message, ts: Date.now() }) | |
| 569 | + thinkingText.value = ''; thinkingContent.value = '' | |
| 570 | + scrollToBottom(); scrollAcpLog(); break | |
| 568 | 571 | } |
| 569 | 572 | } |
| 570 | 573 | ... | ... |