Commit bb089e3f0e690702449a51587c02bfad1c44e6f2
1 parent
86f37a4b
fix: 添加 sendPrompt 120s 空闲超时保护
Claude CLI 遇到 API rate limit/hang 时可能既不输出 stdout 也不输出 stderr, 导致 sendPrompt Promise 永远挂起,前端卡在思考中状态。 - 新增 120s 空闲超时:无 stdout 活动自动 reject - 有 stdout 活动时重置计时器(避免长任务误判) - result 到达时清理计时器
Showing
1 changed file
with
37 additions
and
0 deletions
packages/acp-core/protocol/ClaudeCliProtocol.js
| ... | ... | @@ -85,6 +85,9 @@ class ClaudeCliProtocol extends EventEmitter { |
| 85 | 85 | const lines = this._buffer.split('\n'); |
| 86 | 86 | this._buffer = lines.pop() || ''; |
| 87 | 87 | |
| 88 | + // 有 stdout 活动 → 重置超时计时器 | |
| 89 | + if (this._resetIdleTimer) this._resetIdleTimer(); | |
| 90 | + | |
| 88 | 91 | for (const line of lines) { |
| 89 | 92 | if (!line.trim()) continue; |
| 90 | 93 | try { |
| ... | ... | @@ -182,6 +185,37 @@ class ClaudeCliProtocol extends EventEmitter { |
| 182 | 185 | this._promptResolve = resolve; |
| 183 | 186 | this._promptReject = reject; |
| 184 | 187 | |
| 188 | + // 超时保护:如果 120 秒内没有任何 stdout 活动(API hang/rate limit 静默),自动 reject | |
| 189 | + const IDLE_TIMEOUT_MS = 120_000; | |
| 190 | + let idleTimer = setTimeout(() => { | |
| 191 | + if (this._promptReject) { | |
| 192 | + const err = new Error(`Claude CLI 响应超时 (${IDLE_TIMEOUT_MS / 1000}s 无活动)`); | |
| 193 | + console.error('[ClaudeCliProtocol]', err.message); | |
| 194 | + this._promptReject(err); | |
| 195 | + this._promptResolve = null; | |
| 196 | + this._promptReject = null; | |
| 197 | + this._idleTimer = null; | |
| 198 | + } | |
| 199 | + }, IDLE_TIMEOUT_MS); | |
| 200 | + | |
| 201 | + // 每次收到 stdout 数据时重置超时(说明 CLI 还活着) | |
| 202 | + this._idleTimer = idleTimer; | |
| 203 | + this._resetIdleTimer = () => { | |
| 204 | + if (this._idleTimer) { | |
| 205 | + clearTimeout(this._idleTimer); | |
| 206 | + this._idleTimer = setTimeout(() => { | |
| 207 | + if (this._promptReject) { | |
| 208 | + const err = new Error(`Claude CLI 响应超时 (${IDLE_TIMEOUT_MS / 1000}s 无活动)`); | |
| 209 | + console.error('[ClaudeCliProtocol]', err.message); | |
| 210 | + this._promptReject(err); | |
| 211 | + this._promptResolve = null; | |
| 212 | + this._promptReject = null; | |
| 213 | + this._idleTimer = null; | |
| 214 | + } | |
| 215 | + }, IDLE_TIMEOUT_MS); | |
| 216 | + } | |
| 217 | + }; | |
| 218 | + | |
| 185 | 219 | // Claude CLI stream-json 输入格式 |
| 186 | 220 | const message = { |
| 187 | 221 | type: 'user', |
| ... | ... | @@ -197,8 +231,10 @@ class ClaudeCliProtocol extends EventEmitter { |
| 197 | 231 | try { |
| 198 | 232 | this._child.stdin.write(jsonLine); |
| 199 | 233 | } catch (e) { |
| 234 | + clearTimeout(idleTimer); | |
| 200 | 235 | this._promptResolve = null; |
| 201 | 236 | this._promptReject = null; |
| 237 | + this._idleTimer = null; | |
| 202 | 238 | reject(new Error(`[ClaudeCliProtocol] stdin 写入失败: ${e.message}`)); |
| 203 | 239 | } |
| 204 | 240 | }); |
| ... | ... | @@ -260,6 +296,7 @@ class ClaudeCliProtocol extends EventEmitter { |
| 260 | 296 | }); |
| 261 | 297 | |
| 262 | 298 | if (this._promptResolve) { |
| 299 | + if (this._idleTimer) { clearTimeout(this._idleTimer); this._idleTimer = null; } | |
| 263 | 300 | this._promptResolve(); |
| 264 | 301 | this._promptResolve = null; |
| 265 | 302 | this._promptReject = null; | ... | ... |