Files
qiming/qimingclaw/crates/agent-electron-client/docs/long-running-stability-improvements.md

207 lines
8.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Electron 客户端长期运行稳定性改进
> 日期: 2026-03-18
> 分支: feature/electron-client-0.9
## 背景
QimingClaw 是长期运行的桌面应用(可能持续运行数天/数周)。经过全面代码审查,发现若干资源泄漏、定时器未清理、进程清理不完整等问题,以及网络不稳定时超时过于激进的问题。本次优化共 13 项改动,新增 21 个测试用例。
---
## 第一部分资源泄漏修复6 项)
### 1. Event Forwarders 监听器泄漏修复
**文件**: `src/main/ipc/eventForwarders.ts`
**问题**: 21 个监听器注册在 agentService 上,永不清理。关闭窗口后旧闭包仍引用已销毁的 window。
**修复**:
- 新增模块级 `registeredHandlers[]` 数组,存储每个 `{ event, handler }`
- 导出 `unregisterEventForwarders()`,遍历调用 `agentService.off(event, fn)` 精确移除
- `registerEventForwarders()` 开头先调用 `unregisterEventForwarders()` 保证幂等
### 2. 将 unregister 接入 cleanupAllProcesses
**文件**: `src/main/main.ts`
**修复**: 在 `cleanupAllProcesses()` 中、`agentService.destroy()` 之前调用 `unregisterEventForwarders()`,确保应用退出时所有事件监听器被清理。
### 3. ProcessManager.stop() 补齐 removeAllListeners
**文件**: `src/main/processManager.ts`
**问题**: `kill()``stopAsync()` 都正确移除了 stdout/stderr/stdin 监听器,但 `stop()` 没有。Windows 上可能导致句柄无法释放。
**修复**: 在 `stop()``proc.kill()` 之前加上对 stdout/stderr/stdin/proc 的 `removeAllListeners()`
### 4. SSE heartbeat 增加 error 事件清理
**文件**: `src/main/services/computerServer.ts`
**问题**: heartbeat interval 仅在 `req.on('close')` 时清理。若 response 流出错但 close 未触发interval 泄漏。
**修复**: 添加 `res.on('error', () => { clearInterval(heartbeat); })`
### 5. MemoryFileSync destroy 竞态修复
**文件**: `src/main/services/memory/MemoryFileSync.ts`
**问题**: `destroy()` 调用顺序可能导致异步 `processPendingSync` 访问已清理资源。
**修复**:
- 调整顺序:先设 `this.initialized = false`,再 `clearAllTimers()`,最后 `stopWatcher()`
-`processPendingSync()` 开头增加 `if (!this.initialized) return;` 守卫
### 6. PermissionsPage 定时器泄漏修复
**文件**: `src/renderer/components/pages/PermissionsPage.tsx`
**问题**: click handler 内创建 `setInterval` + `setTimeout`,组件卸载前不会清理。快速点击会导致定时器叠加。
**修复**: 用 `useRef` 存储定时器引用,`useCallback` 封装清理函数,`useEffect` return 中清理。新定时器创建前先清除旧定时器。
---
## 第二部分超时与网络容忍度提升7 项)
所有超时常量已集中到 `src/shared/constants.ts` 统一管理。
### 7. 集中超时常量调整
**文件**: `src/shared/constants.ts`
| 常量 | 旧值 | 新值 | 原因 |
|------|------|------|------|
| `DEFAULT_API_TIMEOUT` | 30s | 60s | 弱网环境下 30s 可能不够 |
| `DEFAULT_SSE_RETRY_DELAY` | 3s | 5s | 初始重试间隔过短,弱网下频繁重连 |
| `DEFAULT_SSE_MAX_RETRY_DELAY` | 30s | 60s | 持续断网时应更慢退避 |
### 8. 退出清理超时增加
| 常量 | 旧值 | 新值 | 原因 |
|------|------|------|------|
| `CLEANUP_TIMEOUT` | 5s | 15s | 多引擎 + 多 MCP 服务器 + memory extraction 可能超过 5 秒 |
**文件**: `src/main/main.ts` — 引用 `CLEANUP_TIMEOUT` 常量替代内联硬编码。
### 9. 进程 SIGTERM→SIGKILL 升级超时增加
| 常量 | 旧值 | 新值 | 原因 |
|------|------|------|------|
| `PROCESS_KILL_ESCALATION_TIMEOUT` | 3s | 5s | 进程若在做网络 I/O 清理可能不够 |
**文件**: `src/main/processManager.ts` — 引用常量替代内联硬编码。
### 10. ACP 会话取消超时增加
| 常量 | 旧值 | 新值 | 原因 |
|------|------|------|------|
| `ACP_ABORT_TIMEOUT` | 10s | 15s | 弱网时 ACP 二进制响应取消命令可能更慢 |
**文件**: `src/main/services/engines/acp/acpEngine.ts` — 引用常量替代内联 `ABORT_TIMEOUT_MS`
### 11. 引擎销毁超时增加
| 常量 | 旧值 | 新值 | 原因 |
|------|------|------|------|
| `ENGINE_DESTROY_TIMEOUT` | 10s | 20s | 多个 MCP 服务器关闭 + 进程清理可能需要更多时间 |
**文件**: `src/main/services/engines/unifiedAgent.ts` — 引用常量替代内联硬编码。
### 12. SSE 心跳间隔放宽
| 常量 | 旧值 | 新值 | 原因 |
|------|------|------|------|
| `DEFAULT_SSE_HEARTBEAT_INTERVAL` | 15s | 30s | 弱网时可能在短暂中断期间丢失连接 |
**文件**: `src/main/services/computerServer.ts` — 引用常量替代内联硬编码。
### 13. 依赖同步添加超时
| 常量 | 新增值 | 原因 |
|------|--------|------|
| `DEPS_SYNC_TIMEOUT` | 120s | 网络异常时 `syncInitDependencies()` 可能永久挂起 |
**文件**: `src/main/bootstrap/startup.ts` — 用 `Promise.race` 包装 120 秒超时,超时后设 `_depsSyncInProgress = false` 并通知 renderer。定时器通过 `.finally()` 正确清理。
---
## 超时常量一览
所有超时常量集中在 `src/shared/constants.ts`
```typescript
// API & SSE
DEFAULT_API_TIMEOUT = 60_000 // API 请求超时
DEFAULT_SSE_RETRY_DELAY = 5_000 // SSE 初始重试延迟
DEFAULT_SSE_MAX_RETRY_DELAY = 60_000 // SSE 最大重试延迟
DEFAULT_SSE_HEARTBEAT_INTERVAL = 30_000 // SSE 心跳间隔
// 进程生命周期
DEFAULT_STARTUP_DELAY = 3_000 // 进程启动延迟
CLEANUP_TIMEOUT = 15_000 // 应用退出清理超时
PROCESS_KILL_ESCALATION_TIMEOUT = 5_000 // SIGTERM→SIGKILL 升级超时
// 引擎 & ACP
ACP_ABORT_TIMEOUT = 15_000 // ACP 会话取消超时
ENGINE_DESTROY_TIMEOUT = 20_000 // 引擎销毁超时
// 依赖管理
DEPS_SYNC_TIMEOUT = 120_000 // 依赖同步超时
```
**约束关系**:
- `PROCESS_KILL_ESCALATION_TIMEOUT < CLEANUP_TIMEOUT`
- `ACP_ABORT_TIMEOUT ≤ ENGINE_DESTROY_TIMEOUT`
- `DEPS_SYNC_TIMEOUT > ENGINE_DESTROY_TIMEOUT > CLEANUP_TIMEOUT`
---
## 测试覆盖
新增 3 个测试文件,共 21 个测试用例:
| 文件 | 用例数 | 覆盖内容 |
|------|--------|----------|
| `src/main/ipc/eventForwarders.test.ts` | 8 | 监听器注册/注销、幂等性、事件转发、注销后静默 |
| `src/main/processManager.test.ts` | 7 | stop() 清理、null-before-kill 顺序、SIGKILL 升级时序、定时器取消 |
| `src/main/services/memory/MemoryFileSync.test.ts` | 3 | destroy 顺序、processPendingSync 守卫、destroy 后 debounce 不触发 |
| `src/shared/constants.test.ts` | +5 | 超时约束关系断言 |
全量测试:**470/470 通过**,构建无错误。
---
## 已有的良好实践(保持不变)
- ProcessRegistry 孤儿进程检测
- 日志轮转(按天 + 按大小7/30 天 TTL
- SSE 事件缓冲max 50, 30s TTL
- SIGTERM→SIGKILL 升级
- SQLite 状态持久化
- ServiceManager 服务启动顺序编排
---
## 变更文件清单
| 文件 | 类型 |
|------|------|
| `src/shared/constants.ts` | 修改 — 新增 6 个超时常量,调整 3 个现有值 |
| `src/main/ipc/eventForwarders.ts` | 修改 — 监听器生命周期管理 |
| `src/main/main.ts` | 修改 — 清理流程集成、引用常量 |
| `src/main/processManager.ts` | 修改 — stop() 补齐监听器清理、引用常量 |
| `src/main/services/computerServer.ts` | 修改 — SSE error 清理、引用常量 |
| `src/main/services/memory/MemoryFileSync.ts` | 修改 — destroy 竞态修复 |
| `src/renderer/components/pages/PermissionsPage.tsx` | 修改 — 定时器泄漏修复 |
| `src/main/services/engines/acp/acpEngine.ts` | 修改 — 引用常量 |
| `src/main/services/engines/unifiedAgent.ts` | 修改 — 引用常量 |
| `src/main/bootstrap/startup.ts` | 修改 — 依赖同步超时 |
| `src/main/ipc/eventForwarders.test.ts` | 新增 |
| `src/main/processManager.test.ts` | 新增 |
| `src/main/services/memory/MemoryFileSync.test.ts` | 新增 |
| `src/shared/constants.test.ts` | 修改 — 新增断言 |
| `src/main/services/engines/acp/acpEngine.test.ts` | 修改 — 适配新超时值 |