1+ /**
2+ * 窗口双向评分规则(纯函数)
3+ *
4+ * 双向打分模型:学习意图正信号(标题关键词/攻略词/进程白名单/几何/前台/记忆)
5+ * 对冲娱乐负分(游戏/影视/直播进程与标题形态),总分决定推荐排序与置信度。
6+ * 系统窗口经黑名单硬过滤;宽泛标题词(Settings/设置)已移除,避免误伤。
7+ *
8+ * @ai -context: 双向评分——负向排除(娱乐特征负分沉底但不误杀)优于单一维度
9+ * 硬排除(游戏攻略/影视纪录片是合法学习内容);意图先验:用户打开课堂助手
10+ * 即声明"采集正在看的学习内容",前台窗口 +80 是最高置信信号。
11+ * @ai -context EN: Bidirectional window scoring. Negative entertainment
12+ * penalties (never hard-filter, so game guides / documentaries survive)
13+ * offset positive learning-intent signals. Foreground window (+80) is the
14+ * strongest intent prior.
15+ */
16+
17+ // ================================================================
18+ // 类型定义
19+ // ================================================================
20+
21+ export type Confidence = 'low' | 'medium' | 'high' ;
22+
23+ export interface WindowSignalInput {
24+ title : string ;
25+ /** 进程可执行文件名(小写,如 chrome.exe);native 不可用时缺失 */
26+ processName ?: string ;
27+ /** 窗口宽高比(width/height) */
28+ aspectRatio ?: number ;
29+ /** 窗口面积占显示器面积比例(0~1) */
30+ areaRatio ?: number ;
31+ alwaysOnTop ?: boolean ;
32+ /** 是否为前台窗口(native GetForegroundWindow 命中) */
33+ isForeground ?: boolean ;
34+ }
35+
36+ export interface WindowScoreResult {
37+ score : number ;
38+ confidence : Confidence ;
39+ /** 命中理由(前端展示用),如 '学习意图' / '进程: wemeet.exe' */
40+ reasons : string [ ] ;
41+ /** true = 命中系统黑名单,应被过滤 */
42+ filtered : boolean ;
43+ /** 娱乐负分合计(供"未被娱乐负分主导"判定,learningScore = score - entertainmentPenalty) */
44+ entertainmentPenalty : number ;
45+ }
46+
47+ // ================================================================
48+ // 规则常量
49+ // ================================================================
50+
51+ /** 置信度分级阈值 */
52+ export const HIGH_CONFIDENCE_MIN = 130 ;
53+ export const MEDIUM_CONFIDENCE_MIN = 25 ;
54+
55+ /** 标题高优先级词(原 windowScorer 迁移,+40/词可叠加) */
56+ const HIGH_PRIORITY_WORDS = [
57+ '网课' , '直播' , '课程' , '课堂' , '学习' , '讲座' , '培训' ,
58+ '腾讯会议' , '钉钉' , 'zoom' , 'teams' , 'meet' , 'webex' ,
59+ 'mooc' , '学堂在线' , '智慧树' , '学习通' , '雨课堂' ,
60+ 'bilibili' , '哔哩哔哩' , 'youtube' , '网易公开课' ,
61+ 'coursera' , 'edx' , 'udemy' ,
62+ ] ;
63+
64+ /** 标题中优先级词(原 windowScorer 迁移,+20/词可叠加) */
65+ const MEDIUM_PRIORITY_WORDS = [
66+ 'chrome' , 'edge' , 'firefox' , 'brave' , 'opera' ,
67+ 'potplayer' , 'vlc' , 'mpv' , 'windows media' ,
68+ 'ev录屏' , 'obs' , 'mpc-hc' ,
69+ ] ;
70+
71+ /** 学习意图正信号词(攻略/教程类,跨场景最强,+60) */
72+ const LEARNING_INTENT_RE =
73+ / 攻 略 | 教 程 | 指 南 | 教 学 | 讲 解 | 解 析 | 入 门 | 进 阶 | 技 巧 | 打 法 | 开 荒 | 机 制 | 评 测 | 实 测 | 心 得 / ;
74+
75+ /** 娱乐标题形态负分词(-40) */
76+ const ENTERTAINMENT_TITLE_RE = / 第 \s * \d + \s * 集 | 剧 场 版 | 预 告 片 | \b m v \b | 演 唱 会 | 番 剧 / ;
77+
78+ /** 进程白名单(+50:会议/视频站客户端) */
79+ const PROCESS_HIGH_WHITELIST = new Set ( [
80+ 'wemeet.exe' , 'dingtalk.exe' , 'zoom.exe' , 'teams.exe' , 'webex.exe' ,
81+ 'bilibili.exe' , 'bililive.exe' , 'youku.exe' , 'iqiyi.exe' ,
82+ ] ) ;
83+
84+ /** 进程白名单(+25:浏览器/播放器) */
85+ const PROCESS_MEDIUM_WHITELIST = new Set ( [
86+ 'chrome.exe' , 'msedge.exe' , 'firefox.exe' , 'brave.exe' , 'opera.exe' ,
87+ 'potplayermini64.exe' , 'vlc.exe' , 'mpv.exe' ,
88+ ] ) ;
89+
90+ /** 娱乐进程负分(-30:游戏/影视客户端/直播平台) */
91+ const PROCESS_ENTERTAINMENT = new Set ( [
92+ 'steam.exe' , 'wegame.exe' , 'leagueclient.exe' , 'league of legends.exe' ,
93+ 'gta5.exe' , 'dota2.exe' , 'csgo.exe' , 'valorant.exe' , 'genshinimpact.exe' ,
94+ 'douyu.exe' , 'huya.exe' , 'netflix.exe' , 'mangotv.exe' ,
95+ ] ) ;
96+
97+ /** 系统窗口进程黑名单(硬过滤) */
98+ const PROCESS_BLACKLIST = new Set ( [
99+ 'explorer.exe' , 'searchhost.exe' , 'startmenuexperiencehost.exe' ,
100+ 'shellexperiencehost.exe' , 'systemsettings.exe' ,
101+ ] ) ;
102+
103+ /** 标题黑名单(硬过滤;保留旧品牌'课伴'防旧版本窗口被采集) */
104+ const TITLE_BLACKLIST = [
105+ 'program manager' , 'taskbar' , 'windows input experience' ,
106+ 'msctfime' , 'default ime' , 'electron' , 'entropy decrease' , '课伴' , '熵减' ,
107+ ] ;
108+
109+ // ================================================================
110+ // 评分实现
111+ // ================================================================
112+
113+ function countWords ( title : string , words : string [ ] ) : number {
114+ let count = 0 ;
115+ for ( const w of words ) {
116+ if ( title . includes ( w ) ) count += 1 ;
117+ }
118+ return count ;
119+ }
120+
121+ /**
122+ * 对单个窗口的标题+系统信号双向打分。
123+ * @returns 总分、置信度、命中理由、过滤标记、娱乐负分
124+ */
125+ export function scoreWindow ( input : WindowSignalInput ) : WindowScoreResult {
126+ const title = ( input . title ?? '' ) . trim ( ) . toLowerCase ( ) ;
127+ const reasons : string [ ] = [ ] ;
128+ let score = 0 ;
129+ let entertainmentPenalty = 0 ;
130+
131+ // 黑名单检测(标题 + 进程)
132+ if ( ! title || TITLE_BLACKLIST . some ( ( w ) => title . includes ( w ) ) ) {
133+ return { score : 0 , confidence : 'low' , reasons : [ ] , filtered : true , entertainmentPenalty : 0 } ;
134+ }
135+ if ( input . processName && PROCESS_BLACKLIST . has ( input . processName ) ) {
136+ return { score : 0 , confidence : 'low' , reasons : [ ] , filtered : true , entertainmentPenalty : 0 } ;
137+ }
138+
139+ // 正向:标题关键词叠加
140+ const highHits = countWords ( title , HIGH_PRIORITY_WORDS ) ;
141+ const mediumHits = countWords ( title , MEDIUM_PRIORITY_WORDS ) ;
142+ score += highHits * 40 + mediumHits * 20 ;
143+ if ( highHits > 0 ) reasons . push ( `标题×${ highHits } ` ) ;
144+ if ( mediumHits > 0 ) reasons . push ( `播放器/浏览器×${ mediumHits } ` ) ;
145+
146+ // 正向:学习意图正信号(+60)
147+ if ( LEARNING_INTENT_RE . test ( title ) ) {
148+ score += 60 ;
149+ reasons . push ( '学习意图' ) ;
150+ }
151+
152+ // 负向:娱乐标题形态(-40)
153+ if ( ENTERTAINMENT_TITLE_RE . test ( title ) ) {
154+ entertainmentPenalty += 40 ;
155+ score -= 40 ;
156+ reasons . push ( '娱乐形态' ) ;
157+ }
158+
159+ // 进程信号
160+ if ( input . processName ) {
161+ if ( PROCESS_HIGH_WHITELIST . has ( input . processName ) ) {
162+ score += 50 ;
163+ reasons . push ( `进程: ${ input . processName } ` ) ;
164+ } else if ( PROCESS_MEDIUM_WHITELIST . has ( input . processName ) ) {
165+ score += 25 ;
166+ reasons . push ( `进程: ${ input . processName } ` ) ;
167+ } else if ( PROCESS_ENTERTAINMENT . has ( input . processName ) ) {
168+ entertainmentPenalty += 30 ;
169+ score -= 30 ;
170+ reasons . push ( '娱乐进程' ) ;
171+ }
172+ }
173+
174+ // 几何信号:视频/PPT 宽高比(1.2~2.4)+30
175+ if ( input . aspectRatio !== undefined && input . aspectRatio > 1.2 && input . aspectRatio < 2.4 ) {
176+ score += 30 ;
177+ reasons . push ( '宽高比' ) ;
178+ }
179+ // 面积占比 ≥30% +20
180+ if ( input . areaRatio !== undefined && input . areaRatio >= 0.3 ) {
181+ score += 20 ;
182+ reasons . push ( '大窗口' ) ;
183+ }
184+ // 置顶 +10
185+ if ( input . alwaysOnTop ) {
186+ score += 10 ;
187+ }
188+ // 前台窗口 +80(意图先验)
189+ if ( input . isForeground ) {
190+ score += 80 ;
191+ reasons . push ( '前台窗口' ) ;
192+ }
193+
194+ const confidence : Confidence =
195+ score >= HIGH_CONFIDENCE_MIN ? 'high' : score >= MEDIUM_CONFIDENCE_MIN ? 'medium' : 'low' ;
196+
197+ return { score, confidence, reasons, filtered : false , entertainmentPenalty } ;
198+ }
0 commit comments