@@ -101,7 +101,12 @@ fn run_second_pass(
101101 let mut inserted = 0usize ;
102102 for ( i, ( start_ms, end_ms) ) in windows. iter ( ) . enumerate ( ) {
103103 if job. is_cancelled ( ) {
104- let _ = app. emit ( "session:refine2:aborted" , session_id) ;
104+ // 取消:已完成的窗保留为 pending 草稿(重跑会清理);payload 与
105+ // 其余终态事件同构(对象含 sessionId——前端监听按对象消费)
106+ let _ = app. emit (
107+ "session:refine2:aborted" ,
108+ serde_json:: json!( { "sessionId" : session_id } ) ,
109+ ) ;
105110 return Ok ( inserted) ;
106111 }
107112 let from = ( * start_ms * sample_rate as u64 / 1000 ) as usize ;
@@ -169,24 +174,46 @@ pub fn second_pass_start(
169174 return Err ( "未找到会话落盘音频(离线精修仅支持有 S4 音频的实时捕获会话)" . to_string ( ) ) ;
170175 }
171176 let job = Pass2Job :: new ( ) ;
177+ // 单锁内原子“检查-注册”(审查 M4:防双击/跨窗口同跑):
178+ // 会话级防重 + 全局单飞(引擎池 ASR worker 串行,多会话并行只会互相排队)
172179 {
173180 let mut jobs = state. second_pass_jobs . lock ( ) . map_err ( |_| "任务注册表锁中毒" . to_string ( ) ) ?;
181+ if jobs. contains_key ( & session_id) {
182+ return Err ( "该会话已有第二遍任务进行中" . to_string ( ) ) ;
183+ }
184+ if !jobs. is_empty ( ) {
185+ return Err ( "已有其他会话的第二遍在跑(引擎串行)——请先等待完成或取消" . to_string ( ) ) ;
186+ }
174187 jobs. insert ( session_id, job. clone ( ) ) ;
175188 }
176189 let app = app. clone ( ) ;
177190 let db = state. db . clone ( ) ;
178191 let engines = state. engines . clone ( ) ;
179192 let path = audio_path. clone ( ) ;
193+ // 线程内取消标志副本(run 移走 job 后 wrapper 仍需读取消态)
194+ let cancel_flag = job. clone ( ) ;
180195 let spawn = std:: thread:: Builder :: new ( )
181196 . name ( "entropy-second-pass" . into ( ) )
182197 . spawn ( move || {
183- let result = run_second_pass ( app. clone ( ) , db. clone ( ) , engines, path, session_id, job) ;
198+ // panic 收尾(审查 M4):catch_unwind 保证注册表清理与终态事件
199+ // ——panic 永不残留 running 卡死态
200+ let result = std:: panic:: catch_unwind ( std:: panic:: AssertUnwindSafe ( || {
201+ run_second_pass ( app. clone ( ) , db. clone ( ) , engines, path, session_id, job)
202+ } ) )
203+ . unwrap_or_else ( |_| {
204+ Err ( "第二遍内部错误(panic)——已清理注册表,请重试" . to_string ( ) )
205+ } ) ;
184206 // 无论成败先移除运行标记(注册表与线程一一对应)
185207 if let Ok ( mut jobs) = app. state :: < AppState > ( ) . second_pass_jobs . lock ( ) {
186208 jobs. remove ( & session_id) ;
187209 }
188210 match result {
189211 Ok ( n) => {
212+ // 取消路径已发 aborted——不再补发 done(避免“已取消”被覆盖闪烁)
213+ if cancel_flag. is_cancelled ( ) {
214+ eprintln ! ( "[SecondPass] 会话 {} 第二遍已取消(保留 {} 条已完成草稿)" , session_id, n) ;
215+ return ;
216+ }
190217 let _ = app. emit (
191218 "session:refine2:done" ,
192219 serde_json:: json!( { "sessionId" : session_id, "proposals" : n } ) ,
0 commit comments