Skip to content

Commit 064f636

Browse files
committed
fix(electron): resolve ELEC2-M1/L1/L2
- ELEC2-M1: MCP manager init failure resets initialized flag and retries with exponential backoff (1s-30s, max 8 attempts) - the stale flag previously disabled MCP silently for the whole run - ELEC2-L1: window recreation (macOS activate) resets sync-before-quit state machine - stale requested=true + completed=false made the new window impossible to close - ELEC2-L2: audio capture runtime degradation re-checks capturing before restarting the provider, eliminating ghost capture when stop() races with degradeToEndpoint
1 parent c00c087 commit 064f636

3 files changed

Lines changed: 48 additions & 0 deletions

File tree

‎client/electron/audioCapture.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,10 @@ export class AudioCapture {
219219
fallback: null,
220220
};
221221
try {
222+
// ELEC2-L2: startWithKind 前二次校验——首次守卫后到此处存在 await 边界,
223+
// stop() 可能已置 capturing=false;继续启动会产生幽灵采集(provider
224+
// 已重启但无人会停止它)。降级结果以 capturing 最新状态为准
225+
if (!this.capturing) return;
222226
await this.startWithKind('endpoint_loopback', this.boundWindow, this.boundSourceId);
223227
} catch (err) {
224228
logger.error('[AudioCapture] 降级到端点环回失败', err);

‎client/electron/mcpManager.ts‎

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,11 @@ import {
3535
/** 单次请求超时(ms)——bridge 无响应时拒绝并清理 pending,防请求悬挂 */
3636
const REQUEST_TIMEOUT_MS = 30_000;
3737

38+
// ELEC2-M1: init 失败后的指数退避重试参数(1s→2s→4s…封顶 30s,最多 8 次)
39+
const INIT_RETRY_BASE_MS = 1000;
40+
const INIT_RETRY_MAX_MS = 30_000;
41+
const INIT_RETRY_MAX_ATTEMPTS = 8;
42+
3843
// ================================================================
3944
// 管理器
4045
// ================================================================
@@ -43,6 +48,9 @@ class McpManager {
4348
private bridge: ChildProcess | null = null;
4449
private initialized = false;
4550
private requestCounter = 0;
51+
/** ELEC2-M1: init 重试计数与定时器(指数退避,环境故障自愈) */
52+
private retryAttempts = 0;
53+
private retryTimer: ReturnType<typeof setTimeout> | null = null;
4654
/** 待响应的请求回调 */
4755
private pending = new Map<string, {
4856
resolve: (value: unknown) => void;
@@ -124,9 +132,33 @@ class McpManager {
124132
} catch (err) {
125133
logger.error('[MCP] Manager init failed', err);
126134
this.killBridge();
135+
// ELEC2-M1: 重置 initialized 并指数退避重试——原实现保留 true 导致
136+
// MCP 功能本次运行永久静默禁用(bridge 故障/环境变量缺失后无自愈机会)
137+
this.initialized = false;
138+
this.scheduleRetryInit();
127139
}
128140
}
129141

142+
/**
143+
* ELEC2-M1: 指数退避重试 init(1s→2s→4s…封顶 30s,最多 8 次)
144+
* 环境类故障(PATH 缺失、bridge 编译产物未就绪)通常可自愈
145+
*/
146+
private scheduleRetryInit(): void {
147+
if (this.retryTimer) return; // 已有重试计划
148+
if (this.retryAttempts >= INIT_RETRY_MAX_ATTEMPTS) {
149+
logger.error('[MCP] Init retry attempts exhausted, giving up');
150+
return;
151+
}
152+
const delay = Math.min(INIT_RETRY_BASE_MS * 2 ** this.retryAttempts, INIT_RETRY_MAX_MS);
153+
this.retryAttempts += 1;
154+
this.retryTimer = setTimeout(() => {
155+
this.retryTimer = null;
156+
if (this.initialized) return; // 已成功,无需重试
157+
logger.info(`[MCP] Retrying init (attempt ${this.retryAttempts}/${INIT_RETRY_MAX_ATTEMPTS})...`);
158+
void this.init().catch(() => {});
159+
}, delay);
160+
}
161+
130162
/**
131163
* 列出指定 server 的所有可用工具
132164
*/
@@ -173,6 +205,12 @@ class McpManager {
173205
async shutdown(): Promise<void> {
174206
logger.info('[MCP] Shutting down MCP Manager...');
175207

208+
// ELEC2-M1: 取消未执行的重试定时器(退出时不再自愈)
209+
if (this.retryTimer) {
210+
clearTimeout(this.retryTimer);
211+
this.retryTimer = null;
212+
}
213+
176214
if (this.bridge) {
177215
try {
178216
// 给 bridge 1 秒时间优雅关闭(避免阻塞退出)

‎client/electron/windowManager.ts‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,12 @@ export function createMainWindow(
7878
isQuittingRef: { value: boolean },
7979
onQuit: () => void,
8080
): BrowserWindow {
81+
// ELEC2-L1: 窗口创建(含 macOS activate 重建)时重置退出前同步状态机——
82+
// 旧状态残留 requested=true 且 completed=false 时,新窗口 close 永远
83+
// preventDefault 无法关闭(同步事件只在旧窗口的 webContents 上发送过)
84+
syncBeforeQuitRequested = false;
85+
syncBeforeQuitCompleted = false;
86+
8187
const win = new BrowserWindow({
8288
width: WINDOW_DEFAULT_WIDTH,
8389
height: WINDOW_DEFAULT_HEIGHT,

0 commit comments

Comments
 (0)