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,6 +85,9 @@ class ClaudeCliProtocol extends EventEmitter { | ||
| 85 | const lines = this._buffer.split('\n'); | 85 | const lines = this._buffer.split('\n'); |
| 86 | this._buffer = lines.pop() || ''; | 86 | this._buffer = lines.pop() || ''; |
| 87 | 87 | ||
| 88 | + // 有 stdout 活动 → 重置超时计时器 | ||
| 89 | + if (this._resetIdleTimer) this._resetIdleTimer(); | ||
| 90 | + | ||
| 88 | for (const line of lines) { | 91 | for (const line of lines) { |
| 89 | if (!line.trim()) continue; | 92 | if (!line.trim()) continue; |
| 90 | try { | 93 | try { |
| @@ -182,6 +185,37 @@ class ClaudeCliProtocol extends EventEmitter { | @@ -182,6 +185,37 @@ class ClaudeCliProtocol extends EventEmitter { | ||
| 182 | this._promptResolve = resolve; | 185 | this._promptResolve = resolve; |
| 183 | this._promptReject = reject; | 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 | // Claude CLI stream-json 输入格式 | 219 | // Claude CLI stream-json 输入格式 |
| 186 | const message = { | 220 | const message = { |
| 187 | type: 'user', | 221 | type: 'user', |
| @@ -197,8 +231,10 @@ class ClaudeCliProtocol extends EventEmitter { | @@ -197,8 +231,10 @@ class ClaudeCliProtocol extends EventEmitter { | ||
| 197 | try { | 231 | try { |
| 198 | this._child.stdin.write(jsonLine); | 232 | this._child.stdin.write(jsonLine); |
| 199 | } catch (e) { | 233 | } catch (e) { |
| 234 | + clearTimeout(idleTimer); | ||
| 200 | this._promptResolve = null; | 235 | this._promptResolve = null; |
| 201 | this._promptReject = null; | 236 | this._promptReject = null; |
| 237 | + this._idleTimer = null; | ||
| 202 | reject(new Error(`[ClaudeCliProtocol] stdin 写入失败: ${e.message}`)); | 238 | reject(new Error(`[ClaudeCliProtocol] stdin 写入失败: ${e.message}`)); |
| 203 | } | 239 | } |
| 204 | }); | 240 | }); |
| @@ -260,6 +296,7 @@ class ClaudeCliProtocol extends EventEmitter { | @@ -260,6 +296,7 @@ class ClaudeCliProtocol extends EventEmitter { | ||
| 260 | }); | 296 | }); |
| 261 | 297 | ||
| 262 | if (this._promptResolve) { | 298 | if (this._promptResolve) { |
| 299 | + if (this._idleTimer) { clearTimeout(this._idleTimer); this._idleTimer = null; } | ||
| 263 | this._promptResolve(); | 300 | this._promptResolve(); |
| 264 | this._promptResolve = null; | 301 | this._promptResolve = null; |
| 265 | this._promptReject = null; | 302 | this._promptReject = null; |