Skip to content

Commit 6d74a0f

Browse files
wehosHongzhi Wenclaude
authored
fix(voice): session 启动失败后等麦克风 settle 再 teardown,修复重试点麦无反应 (#1548)
* fix(voice): session 启动失败后等麦克风初始化 settle 再 teardown start_session 失败时 Promise.all 会立刻 reject 触发外层 catch 做 UI teardown,但 startMicCapture 往往还在 await getUserMedia/AudioWorklet, 随后才 settle,会把 S.isRecording 和浮动麦按钮重新写成"录音中"。结果 没有真实 session 却显示在录音,下一次点麦被浮动按钮当作 toggle-off (关麦)静默吞掉,看不到任何 banner 或提示。 改为失败路径在 rethrow 前先 await startMicCapture 彻底 settle,让外层 catch 的 teardown 收尾,状态归零(非录音、按钮 off),重试点麦能真正 重新发起 session。顺带修掉失败路径上 stopRecording 因 isRecording 尚 未置位而提前 return、导致麦克风流泄漏的问题。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(voice): 给失败路径的 startMicCapture 等待加超时上限 上一版无界 await micCapturePromise:若 startMicCapture 卡在 getUserMedia 权限弹窗(用户忽略),promise 永久 pending,外层 teardown 永远到不了, mic 按钮卡 disabled、isMicStarting 不复位——把幽灵录音态换成了永久卡死态。 改成 Promise.race 3s 超时。settle 在界内则照常等它收尾;卡死则超时后照常 teardown——此时 startMicCapture 尚未走到置 isRecording 那步,无录音态可复活。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(voice): 用 token 守卫的补充 teardown 取代有界等待 上一版 Promise.race 3s 超时在两个边界互相矛盾:mic 初始化超过 3s 时超时 会漏掉、让幽灵录音态复活(Codex P2);而无界等待又会在 getUserMedia 权限 弹窗被忽略时卡死 teardown(前一个 Codex P2)。任何有限超时都堵不住其中 一个窗口。 改为:失败立刻 rethrow 让外层 catch 即时 teardown(无卡死),同时挂一个 带 token 守卫的补充 teardown——startMicCapture 无论多晚 settle,只要它确实 写下了幽灵录音态(isRecording 为真且 voiceChatActive 为假),且期间没有更 新的开麦尝试(token 未变),就再清一次。token 变了或已有在线 session(如 CHARACTER_DISCONNECTED 自动重启成功)则不动,避免误杀新 session。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(voice): 改为先确认 session 再开麦,从根上消除幽灵录音竞态 前两版试图在外层 catch 里追平并发的 startMicCapture(先有界等待、后 token 守卫的补充 teardown),但因为 isRecording/stream/按钮是共享状态、 无归属标记,并发尝试无法靠单个 bool 区分,每补一个边界 Codex 就能再找出 一个:超时漏掉慢初始化、卡死在被忽略的权限弹窗、重试后旧 capture 落地仍 留幽灵态。 改为串行:先 await sessionStartPromise,成功后才 await startMicCapture—— 与 CHARACTER_DISCONNECTED 自动重启路径(app-websocket.js)一致的写法。 session 启动失败时 startMicCapture 根本不会被调用,不存在"mic 在 teardown 之后才 settle、把 UI 写回录音中"的竞态,by construction 无幽灵态、无卡死、 无需 token/补充 teardown。代价是失去几毫秒的开麦并行(权限已预授予,可忽略)。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(voice): 给点击瞬间的 syncFloatingMicButtonState 调用补 typeof 守卫 mic 点击 handler 里这处是点击瞬间立即执行、早于任何安全检查的首个调用, 也是全项目里唯一没带 typeof 守卫的 syncFloatingMicButtonState 调用。虽然 按钮创建晚于 app-ui 加载、实际不会未定义,但与全项目约定对齐,消除隐患。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Hongzhi Wen <cartabio.coder1@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent a4d4dc6 commit 6d74a0f

1 file changed

Lines changed: 10 additions & 6 deletions

File tree

static/app-buttons.js

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1376,7 +1376,7 @@
13761376

13771377
// Immediately activate
13781378
micButton.classList.add('active');
1379-
window.syncFloatingMicButtonState(true);
1379+
if (typeof window.syncFloatingMicButtonState === 'function') window.syncFloatingMicButtonState(true);
13801380
window.isMicStarting = true;
13811381
S.voiceStartPending = true;
13821382
micButton.disabled = true;
@@ -1496,20 +1496,24 @@
14961496
}
14971497
}, 15000);
14981498

1499-
// Parallel: wait for session + init mic
1499+
// Init mic only after the session is confirmed started
15001500
try {
15011501
await window.showCurrentModel();
15021502
window.showStatusToast(window.t ? window.t('app.initializingMic') : '\u6B63\u5728\u521D\u59CB\u5316\u9EA6\u514B\u98CE...', 3000);
15031503

1504-
await Promise.all([
1505-
sessionStartPromise,
1506-
window.startMicCapture()
1507-
]);
1504+
// 先确认 session 启动成功,再开麦。与 CHARACTER_DISCONNECTED 自动
1505+
// 重启路径(app-websocket.js)一致的串行写法:session 启动失败时
1506+
// startMicCapture 根本不会被调用,不存在"mic 在外层 catch teardown
1507+
// 之后才 settle、把 UI 写回录音中"的竞态,也就不需要 token / 补充
1508+
// teardown 去追平它。
1509+
await sessionStartPromise;
15081510

15091511
if (window.sessionTimeoutId) {
15101512
clearTimeout(window.sessionTimeoutId);
15111513
window.sessionTimeoutId = null;
15121514
}
1515+
1516+
await window.startMicCapture();
15131517
} catch (error) {
15141518
if (window.sessionTimeoutId) {
15151519
clearTimeout(window.sessionTimeoutId);

0 commit comments

Comments
 (0)