@@ -36,6 +36,13 @@ pub struct CaptureWindow {
3636 /// 站点首页标记(2026-08 用户需求:B站首页等无视频内容落地页,
3737 /// 前端显示"首页"标签且不进入推荐;仍可手动选择兜底)
3838 pub is_homepage : bool ,
39+ /// v0.19.2:枚举 z 序(EnumWindows 自上而下,0=最顶)。推荐区同级按此
40+ /// 稳定排序——"播放中/当前活跃窗口靠前"的零成本近似(真播放检测需
41+ /// OCR/音频轮询,成本高,登记观察项)。
42+ pub z_order : u32 ,
43+ /// v0.19.2:系统/工具窗口(终端/资源管理器/记事本等——前端默认过滤,
44+ /// 可开关找回;判定见 window_filter::is_system_window)
45+ pub system_window : bool ,
3946}
4047
4148/// 标题关键词评分表:(关键词, 权重, 原因)。
@@ -54,6 +61,10 @@ const TITLE_KEYWORDS: &[(&str, u32, &str)] = &[
5461 ( "网课" , 80 , "网课关键词" ) ,
5562 ( "zoom" , 70 , "Zoom 会议" ) ,
5663 ( "钉钉" , 60 , "钉钉" ) ,
64+ // v0.19.2(用户实测):抖音/快手桌面端标题固定为平台名——补表进推荐区
65+ ( "抖音" , 80 , "抖音视频" ) ,
66+ ( "douyin" , 80 , "抖音视频" ) ,
67+ ( "快手" , 60 , "快手视频" ) ,
5768 ( "课程" , 50 , "课程关键词" ) ,
5869 ( "教程" , 50 , "教程关键词" ) ,
5970 ( "学习" , 30 , "学习关键词" ) ,
@@ -66,6 +77,10 @@ const PROCESS_KEYWORDS: &[(&str, u32, &str)] = &[
6677 ( "mpv" , 60 , "mpv 播放器" ) ,
6778 ( "kmplayer" , 60 , "KMPlayer 播放器" ) ,
6879 ( "wmplayer" , 50 , "Windows 播放器" ) ,
80+ // v0.19.2(用户实测):抖音/快手/独立 B站客户端进程名(窗口标题不可靠时兜底)
81+ ( "douyin" , 80 , "抖音客户端" ) ,
82+ ( "kuaishou" , 60 , "快手客户端" ) ,
83+ ( "bilibili" , 90 , "B站客户端" ) ,
6984 ( "chrome" , 40 , "Chrome 浏览器" ) ,
7085 ( "msedge" , 40 , "Edge 浏览器" ) ,
7186 ( "firefox" , 40 , "Firefox 浏览器" ) ,
@@ -209,18 +224,35 @@ pub fn list_capture_windows() -> Vec<CaptureWindow> {
209224 let ok = unsafe { GetWindowRect ( HWND ( * id as * mut core:: ffi:: c_void ) , & mut rect) } ;
210225 ok. is_ok ( ) && crate :: window_filter:: has_capturable_size ( rect. right - rect. left , rect. bottom - rect. top )
211226 } )
212- . map ( |( id, title, pid) | {
227+ // enumerate 序号即 z 序(EnumWindows 自上而下)——0=最顶窗口
228+ . enumerate ( )
229+ . map ( |( z_order, ( id, title, pid) ) | {
213230 let process_name = unsafe { process_name_of ( pid) } ;
214231 let ( score, reasons) = score_window ( & title, & process_name) ;
215232 let is_homepage = crate :: window_filter:: is_site_homepage ( & title) ;
216233 // 站点首页降权:移出推荐(评分清零 + 原因标注),仍保留可手动选择
217234 let ( score, reasons) =
218235 crate :: window_filter:: demote_homepage ( score, reasons, & title) ;
219- CaptureWindow { id, title, process_name, pid, score, reasons, is_homepage }
236+ // v0.19.2:系统/工具窗口标记(终端/资源管理器等——前端默认过滤
237+ // 可开关找回;不影响其余字段,供 UI 灰显与计数)
238+ let system_window =
239+ crate :: window_filter:: is_system_window ( & title, & process_name) ;
240+ CaptureWindow {
241+ id,
242+ title,
243+ process_name,
244+ pid,
245+ score,
246+ reasons,
247+ is_homepage,
248+ z_order : z_order as u32 ,
249+ system_window,
250+ }
220251 } )
221252 . collect ( ) ;
222- // 推荐窗口(高分)在前,同级按标题稳定排序
223- windows. sort_by ( |a, b| b. score . cmp ( & a. score ) . then_with ( || a. title . cmp ( & b. title ) ) ) ;
253+ // 推荐在前(score 降序);同级按 z 序(越靠顶越新活跃——"播放中置顶"的
254+ // 零成本近似;标题排序会让活跃窗口埋没在同分浏览器标签海里)
255+ windows. sort_by ( |a, b| b. score . cmp ( & a. score ) . then_with ( || a. z_order . cmp ( & b. z_order ) ) ) ;
224256 windows
225257}
226258
@@ -292,4 +324,19 @@ mod tests {
292324 // Assert
293325 assert_eq ! ( upper, lower) ;
294326 }
327+
328+ #[ test]
329+ fn douyin_titles_and_process_score_high ( ) {
330+ // v0.19.2(用户实测):抖音标题固定为"抖音"/进程 douyin——进推荐区
331+ let ( t, tr) = score_window ( "抖音" , "douyin" ) ;
332+ assert ! ( t >= 80 , "标题命中: {}" , t) ;
333+ assert ! ( tr. iter( ) . any( |r| r. contains( "抖音" ) ) ) ;
334+ let ( p, pr) = score_window ( "视频" , "douyin" ) ;
335+ assert ! ( p >= 80 , "进程命中: {}" , p) ;
336+ assert ! ( pr. iter( ) . any( |r| r. contains( "抖音" ) ) ) ;
337+ // 标题不含平台词但进程为 B站客户端 → 仍可推荐
338+ let ( b, br) = score_window ( "一些课程名称" , "bilibili" ) ;
339+ assert ! ( b >= 90 ) ;
340+ assert ! ( br. iter( ) . any( |r| r. contains( "B站" ) ) ) ;
341+ }
295342}
0 commit comments