@@ -121,8 +121,9 @@ export default function ClassroomPage({ onOpenSessions }: { onOpenSessions?: (se
121121 warmUp ( ) ;
122122 // TD-004:课堂助手页常驻挂载(display:none 切换不卸载)——本 cleanup 只
123123 // 在应用卸载/StrictMode replay 时触发,**不在此释放**:release 会取消正在
124- // 加载的预热线程造成 dev 双加载与"已完成→随即取消"误读;真实回收由后端
125- // 15min TTL 与显式 release_live_prepare 命令承担(浮窗/退出路径保留)
124+ // 加载的预热线程造成 dev 双加载与"已完成→随即取消"误读;页面无卸载时机,
125+ // 引擎回收由 15min TTL 兜底(release_live_prepare 命令保留供未来显式
126+ // 回收扩展,当前无调用方——v0.19.3 审查 LOW-2 注释如实化)
126127 // eslint-disable-next-line react-hooks/exhaustive-deps
127128 } , [ ] ) ;
128129
@@ -148,11 +149,16 @@ export default function ClassroomPage({ onOpenSessions }: { onOpenSessions?: (se
148149 if ( e . payload === "recording" ) {
149150 // 后端确认录制中:保持/恢复活动态(invoke resolve 可能更早到达);
150151 // v0.19.2:此刻引擎真正就绪、音频与画面同刻启动——结束启动过渡态
152+ liveActiveRef . current = true ; // ref 同步直写——防 resolve 读旧值(审查 LOW-1)
151153 setLiveActive ( true ) ;
152154 setStopping ( false ) ;
153155 setStarting ( false ) ;
156+ // v0.19.3 审查 MED-1:starting→recording 迁移统一覆写文案——
157+ // 非就绪启动成功路径不得残留「引擎就绪中…」常驻状态行
158+ setStatus ( "实时捕获已开始" ) ;
154159 } else {
155160 // stopped / failed:会话已结束
161+ liveActiveRef . current = false ;
156162 setLiveActive ( false ) ;
157163 setLiveSessionId ( null ) ;
158164 setStopping ( false ) ;
@@ -322,9 +328,17 @@ export default function ClassroomPage({ onOpenSessions }: { onOpenSessions?: (se
322328 if ( starting ) return ; // 防双击双会话(后端 active 检查前即挡住)
323329 setLiveError ( "" ) ;
324330 setStarting ( true ) ;
331+ // v0.19.3 审查 LOW-2:点击时重同步一次预热状态——TTL 静默回收后本地
332+ // prepareState 可能仍为陈旧 'ready'(wasEngineReady 误判 → 控件提前/
333+ // 丢音频头);prepare 幂等:已有就绪秒返,无预备则新起加载(start 同槽
334+ // 有界等待,单引擎语义不变)
335+ const freshPrepare = await invoke < PrepareState > ( "prepare_live_session" )
336+ . then ( ( s ) => s as PrepareState )
337+ . catch ( ( ) => "idle" as PrepareState ) ;
338+ setPrepareState ( freshPrepare ) ;
325339 // 就绪态点击=交接路径(引擎已加载)——resolve 即已开录,可直接置活动;
326340 // 非就绪态 resolve 可能只是"已排队/内联加载中"——等 recording 事件
327- const wasEngineReady = prepareState === "ready" ;
341+ const wasEngineReady = freshPrepare === "ready" ;
328342 try {
329343 const title = selectedWindow ? selectedWindow . title . slice ( 0 , NOTE_TITLE_MAX_LEN ) : "实时课堂" ;
330344 const id = await invoke < number > ( "start_live_session" , {
@@ -354,6 +368,7 @@ export default function ClassroomPage({ onOpenSessions }: { onOpenSessions?: (se
354368 if ( String ( e ) . includes ( "已有进行中的实时会话" ) ) {
355369 try {
356370 const s = await invoke < LiveSessionStatus > ( "live_session_status" ) ;
371+ liveActiveRef . current = s . active ;
357372 setLiveActive ( s . active ) ;
358373 setLiveSessionId ( s . sessionId ) ;
359374 setLiveError ( s . active ? "检测到采集仍在进行,已恢复状态;如需重启请先停止" : "状态已恢复" ) ;
@@ -366,6 +381,34 @@ export default function ClassroomPage({ onOpenSessions }: { onOpenSessions?: (se
366381 }
367382 } ;
368383
384+ // v0.19.3 审查 MED-2:starting 过渡态看门狗(对齐 TD-042 stopping 先例)——
385+ // recording/error/stopped 事件全部丢失时 20s(>后端 15s 上限)后查询真实
386+ // 状态收口:恢复活动态或退出等待态,避免"按钮永久禁用且无停止出口"死锁
387+ useEffect ( ( ) => {
388+ if ( ! starting || liveActive ) return ;
389+ const timer = setTimeout ( ( ) => {
390+ void ( async ( ) => {
391+ try {
392+ const s = await invoke < LiveSessionStatus > ( "live_session_status" ) ;
393+ if ( s . active ) {
394+ liveActiveRef . current = true ;
395+ setLiveActive ( true ) ;
396+ setLiveSessionId ( s . sessionId ) ;
397+ setStatus ( "检测到采集进行中,已恢复状态(引擎就绪后同刻开录)" ) ;
398+ } else {
399+ setLiveSessionId ( null ) ;
400+ setStatus ( "启动状态未确认——引擎未开录;可直接重试(就绪即秒开)" ) ;
401+ }
402+ } catch ( err ) {
403+ setStatus ( `启动状态查询失败: ${ err } ——请重试` ) ;
404+ } finally {
405+ setStarting ( false ) ;
406+ }
407+ } ) ( ) ;
408+ } , 20_000 ) ;
409+ return ( ) => clearTimeout ( timer ) ;
410+ } , [ starting , liveActive ] ) ;
411+
369412 /** 停止实时捕获 */
370413 const stopLive = async ( ) => {
371414 // 停止过渡期:面板保持显示(live:status stopped 到达后由监听清除)
0 commit comments