|
| 1 | +# 课堂助手视频窗口识别优化设计 |
| 2 | + |
| 3 | +> 编制日期:2026-08-16 |
| 4 | +> 状态:已获用户确认(第 1~3 节逐节评审通过) |
| 5 | +> 范围:仅 Windows 平台增强;macOS/其他平台保持现有标题关键词逻辑不变 |
| 6 | +> 配套:`docs/Foresight/classroom-recognition-maturity-audit.md`(识别链路成熟度审计)、`docs/Foresight/classroom-assistant-optimization-roadmap.md`(P0-P2 路线图) |
| 7 | +
|
| 8 | +--- |
| 9 | + |
| 10 | +## 一、背景与目标 |
| 11 | + |
| 12 | +### 1.1 现状问题 |
| 13 | + |
| 14 | +课堂助手的目标窗口识别当前为纯标题关键词一票制评分(`client/electron/windowScorer.ts`,131 行): |
| 15 | + |
| 16 | +- **推荐不准**:标题命中高优先级词 +100、中优先级 +50、黑名单直接过滤;命中即返回,无叠加、无置信度;标题无关键词的窗口(如「第3章 线性代数 - Google Chrome」)直接掉进「显示全部」折叠区。 |
| 17 | +- **重复操作多**:同一门课每次都要重新选窗口,无历史记忆、无自动选中。 |
| 18 | +- **联动链路弱**:课程名提取(`useWindowWatcher` 正则)与内容分类(`contentClassifier` 标题规则)均只依赖标题,窗口识别错了全链路跟着错。 |
| 19 | +- **监听盲区**:3s 轮询仅对窗口 id 集合做 diff,标题变化(如会议未读角标)不触发推送。 |
| 20 | +- **黑名单误伤**:`Settings`/`设置`/`Electron` 等宽泛词可能误伤合法窗口。 |
| 21 | + |
| 22 | +### 1.2 核心洞察(头脑风暴结论) |
| 23 | + |
| 24 | +1. **正向识别内容语义是死路**:化妆/手工类教程标题往往无特异性(「新手必学!3分钟搞定日常妆」不含课程词),窗口层面无法语义判定。 |
| 25 | +2. **意图先验**:用户打开课堂助手 = 声明「我要采集正在看的学习内容」,这是最强先验。设计只需排除「一定非学习」的窗口(负向证伪),灰区交给前台窗口/记忆/唯一候选补位。 |
| 26 | +3. **游戏攻略是排除通道的陷阱**:游戏教程/攻略视频是合法学习内容,且标题几乎自带特征词(攻略/教程/打法)——单一维度硬排除必误伤,必须双向打分对冲。 |
| 27 | + |
| 28 | +### 1.3 目标 |
| 29 | + |
| 30 | +| 维度 | 目标 | |
| 31 | +|---|---| |
| 32 | +| 推荐准确度 | 标题无特异性场景(化妆/手工/攻略视频)仍可推荐或自动选中 | |
| 33 | +| 交互体验 | 高置信度自动选中 + toast 可更换,二次用户零选择启动 | |
| 34 | +| 联动链路 | 窗口识别 → 课程名回填 → 热词词表自动加载;内容分类接入进程信号 | |
| 35 | +| 工程质量 | 纯函数层全覆盖单测;信号源全部可选,缺失时行为与现状一致(不回归) | |
| 36 | + |
| 37 | +--- |
| 38 | + |
| 39 | +## 二、架构设计 |
| 40 | + |
| 41 | +三层拆分,`windowScorer.ts` 降为组合入口: |
| 42 | + |
| 43 | +``` |
| 44 | +┌─ 信号层 client/electron/windowSignals.ts(新增)─────────────┐ |
| 45 | +│ enrich(sources) → 为每个窗口附加信号 │ |
| 46 | +│ · HWND 解析:source id "window:<HWND>:0" → HWND │ |
| 47 | +│ · 进程名:native.listAudioWindows() 按 HWND 建索引匹配 │ |
| 48 | +│ · 几何:native 扩展字段(rect → 宽高比/面积占比/置顶) │ |
| 49 | +│ · 前台窗口:native 新增 GetForegroundWindow 查询 │ |
| 50 | +│ · 优雅降级:native 不可用 → 仅标题信号(= 现状行为) │ |
| 51 | +└──────────────────────────────────────────────────────────────┘ |
| 52 | +┌─ 规则层 client/electron/windowRules.ts(新增,纯函数)───────┐ |
| 53 | +│ scoreTitle(标题叠加计分)/ scoreProcess(进程白名单) │ |
| 54 | +│ scoreLearningIntent(攻略正信号)/ scoreEntertainment(负分)│ |
| 55 | +│ scoreGeometry(宽高比/面积/置顶/全屏)/ 系统黑名单 / 置信度 │ |
| 56 | +└──────────────────────────────────────────────────────────────┘ |
| 57 | +┌─ 记忆层 client/electron/windowHistory.ts(新增)─────────────┐ |
| 58 | +│ recordChoice / lookupBoost / clear(SQLite 表 window_memory)│ |
| 59 | +│ 标题模板化(会议号/章节数字归一)→ hash 维度 │ |
| 60 | +└──────────────────────────────────────────────────────────────┘ |
| 61 | +组合入口 client/electron/windowScorer.ts(改造,导出兼容) |
| 62 | +``` |
| 63 | + |
| 64 | +**设计原则**: |
| 65 | +- 信号源全部可选:native 缺失/失败时自动退化为纯标题逻辑,不改变现有行为路径。 |
| 66 | +- 规则层零副作用纯函数,全部可单测(项目 AI 编程规范 §1 副作用隔离)。 |
| 67 | +- 单文件 ≤300 行(§1),`@ai-context` 中英双语注释(§3)。 |
| 68 | + |
| 69 | +--- |
| 70 | + |
| 71 | +## 三、评分模型(双向打分) |
| 72 | + |
| 73 | +### 3.1 信号与分值 |
| 74 | + |
| 75 | +| 通道 | 信号 | 规则 | 分值 | |
| 76 | +|---|---|---|---| |
| 77 | +| 正向 | 标题·高优先级 | 网课/会议/视频站关键词(保留现有列表) | +40/词,可叠加 | |
| 78 | +| 正向 | 标题·中优先级 | 浏览器/播放器(保留现有列表) | +20/词,可叠加 | |
| 79 | +| 正向 | **学习意图正信号**(新增) | 标题含 `攻略/教程/指南/教学/讲解/解析/入门/进阶/技巧/打法/开荒/机制/评测/实测/心得` | **+60** | |
| 80 | +| 正向 | 进程白名单 | WeMeet.exe / DingTalk / Zoom / Teams / 视频站客户端 | +50 | |
| 81 | +| 正向 | 进程白名单 | chrome / edge / firefox / 播放器 | +25 | |
| 82 | +| 正向 | 几何 | 宽高比 1.2~2.4(视频/PPT 特征) | +30 | |
| 83 | +| 正向 | 几何 | 窗口面积 ≥30% 显示器 | +20 | |
| 84 | +| 正向 | 几何 | 置顶 +10 / 全屏 +15 | — | |
| 85 | +| 正向 | **前台窗口**(新增) | GetForegroundWindow 命中 | **+80** | |
| 86 | +| 正向 | 记忆 boost | min(useCount,5)×6 + recency(7 天内 +10 / 30 天内 +5) | 封顶 +40 | |
| 87 | +| 负向 | **娱乐负分**(新增) | 游戏进程(steam/WeGame/客户端)、影视客户端进程、直播平台进程 | −30 | |
| 88 | +| 负向 | **娱乐负分**(新增) | 标题形态:`第N集/剧场版/预告片/MV/演唱会/番剧` | −40 | |
| 89 | +| 过滤 | 系统黑名单 | 保留现有硬黑名单(Program Manager/Taskbar/自窗口含旧品牌'课伴'等);**移除** `Settings`/`设置` 等宽泛词 | 过滤 | |
| 90 | + |
| 91 | +### 3.2 核心语义 |
| 92 | + |
| 93 | +- **双向打分而非硬排除**:正信号强过负分就进推荐,负分沉底但不误杀(爱奇艺纪录片:进程 −30 + 标题「纪录片/公开课」→ 仍可推荐;爱奇艺剧集:进程 −30 + 标题「第12集」−40 → 沉底)。 |
| 94 | +- **游戏攻略完整路径**: |
| 95 | + - 浏览器看攻略视频:+60 攻略 + 25 浏览器 + 前台/记忆 → high,自动选中 ✓ |
| 96 | + - 直播平台看攻略直播:+60 攻略 −30 娱乐 + 80 前台 → 推荐前列 ✓ |
| 97 | + - 纯娱乐游戏/直播:−30 娱乐、无正信号 → 沉底「显示全部」 ✓ |
| 98 | +- **标题无特异性场景**(化妆教程):标题 0 分,但 进程 +25 + 几何 +45~65 + 记忆(标题模板 `{视频标题} - bilibili.com` 稳定)+10~40 → medium~high;首次无记忆时靠前台窗口 +80 与唯一候选规则补位。 |
| 99 | + |
| 100 | +### 3.3 置信度分级 |
| 101 | + |
| 102 | +| 级别 | 总分 | 用途 | |
| 103 | +|---|---|---| |
| 104 | +| high | ≥130 | 自动选中候选 | |
| 105 | +| medium | 70~129 | 推荐区候选 | |
| 106 | +| low | <70 | 折叠区(显示全部) | |
| 107 | + |
| 108 | +分值阈值实现时校准,以单测断言为准。 |
| 109 | + |
| 110 | +### 3.4 输出契约 |
| 111 | + |
| 112 | +`ScoredWindow` 扩展:保留 `id/title/thumbnail/score/matched`(兼容),新增 `processName?`、`confidence?: 'low'|'medium'|'high'`、`reasons?: string[]`(多个命中理由,替代单值 matched 的展示;matched 仍填充最高权重词保证旧 UI 兼容)。 |
| 113 | + |
| 114 | +--- |
| 115 | + |
| 116 | +## 四、自动选中交互(渲染层 `useWindowWatcher`) |
| 117 | + |
| 118 | +窗口列表就绪后(`screen_windows_changed` / 首次 `screen_list_windows`),**仅在未选中任何窗口时评估**(已有选中不覆盖、不重复 toast): |
| 119 | + |
| 120 | +``` |
| 121 | +top1 满足任一 → 自动选中 + toast「已自动选择:<标题>」+ 卡片显示「更换」 |
| 122 | + ① high 置信度(≥130) |
| 123 | + ② 前台窗口 && 总分 ≥60(前台路径放宽阈值)&&(记忆命中 或 唯一候选) |
| 124 | + ③ medium 且 记忆命中 且 唯一视频类候选 |
| 125 | +``` |
| 126 | + |
| 127 | +「唯一视频类候选」定义:系统黑名单过滤后,总分 ≥70 且未被娱乐负分主导(总分 − 娱乐负分 ≥ 60)的窗口仅此一个。 |
| 128 | + |
| 129 | +不满足 → 仅置顶推荐区,不自动选中(保守,防误采)。 |
| 130 | + |
| 131 | +**误采兜底**:自动选中只改选中态,不启动采集;开始按钮在用户控制下;卡片「更换」可一键改选(现有能力)。 |
| 132 | + |
| 133 | +--- |
| 134 | + |
| 135 | +## 五、历史记忆(`windowHistory.ts`) |
| 136 | + |
| 137 | +### 5.1 存储 |
| 138 | + |
| 139 | +- 主进程 SQLite 新表 `window_memory`(复用既有 sqliteService 基建)。 |
| 140 | +- 维度:`进程名 + 标题模板 hash`(标题归一化:会议号/章节数字/未读数 → 占位符,如「{视频标题} - bilibili.com」)。 |
| 141 | +- 字段:`processName`、`titleTemplate`、`titleHash`、`courseName`、`useCount`、`lastUsedAt`。 |
| 142 | + |
| 143 | +### 5.2 写入 |
| 144 | + |
| 145 | +- 用户**手动**选择窗口时记录(新增 IPC `window_memory_record`,safeHandle 包装 + channels.ts 登记 + preload 白名单,SEC-005)。 |
| 146 | +- 自动选中同样累加 useCount。 |
| 147 | +- 记忆关联课程名:选择时若 courseMeta.courseName 已确定,随记录写入。 |
| 148 | + |
| 149 | +### 5.3 读取与淘汰 |
| 150 | + |
| 151 | +- 评分时按进程名模糊 + 标题模板精确命中 → boost 分(见 3.1)。 |
| 152 | +- 上限 100 条 LRU 淘汰;新增 IPC `window_memory_clear` 支持设置页清空。 |
| 153 | + |
| 154 | +--- |
| 155 | + |
| 156 | +## 六、联动闭环 |
| 157 | + |
| 158 | +### 6.1 课程名提取(`useWindowWatcher`) |
| 159 | + |
| 160 | +优先级:**记忆回填(detectedBy: 'memory')> AI 首帧识别(既有 courseDetector)> 标题正则兜底(现有 COURSE_KEYWORDS,补充攻略类词)**。 |
| 161 | + |
| 162 | +`captureTypes.ts` 的 `CourseMeta.detectedBy` 类型同步扩展 `'memory'` 状态(现为 `'manual' | 'window_title' | 'ai'`)。 |
| 163 | + |
| 164 | +### 6.2 热词词表自动加载 |
| 165 | + |
| 166 | +记忆回填或自动识别出课程名 → 课程名变化联动加载该课程热词词表(P1-3 已落地「会话关联课程后自动应用对应词表」,回填即触发,零新增)。 |
| 167 | + |
| 168 | +### 6.3 内容分类(`contentClassifier`) |
| 169 | + |
| 170 | +`classifyByTitle` 增加可选 `processName` 入参: |
| 171 | +- 进程名命中软件名单(photoshop.exe 等)→ software_skill。 |
| 172 | +- 命中游戏/直播进程 → 交给标题攻略词判定(复用学习意图正信号词表)。 |
| 173 | + |
| 174 | +--- |
| 175 | + |
| 176 | +## 七、监听增强(`screenCaptureHandlers.ts`) |
| 177 | + |
| 178 | +- diff 从「id 集合」升级为「`id|title` 集合」→ 标题变化(未读角标/会议状态)也触发推送。 |
| 179 | +- 缩略图保持现状(仅 hasChanged 时才生成,已是最优,不改动)。 |
| 180 | + |
| 181 | +--- |
| 182 | + |
| 183 | +## 八、错误处理与降级 |
| 184 | + |
| 185 | +| 故障 | 行为 | |
| 186 | +|---|---| |
| 187 | +| native 模块未加载(未编译/非 Windows) | 信号层跳过进程/几何/前台信号,退化为纯标题逻辑(= 现状),自动选中仅接受 high 标题置信度 | |
| 188 | +| SQLite 记忆读写失败 | 记忆层 try/catch 静默降级,无 boost 分,不影响评分 | |
| 189 | +| 前台窗口查询失败 | 前台信号缺失,其余信号照常 | |
| 190 | +| 全部信号缺失(极端) | 输出与现状完全一致(标题关键词排序),保证不回归 | |
| 191 | + |
| 192 | +--- |
| 193 | + |
| 194 | +## 九、测试计划(vitest) |
| 195 | + |
| 196 | +| 文件 | 覆盖点 | |
| 197 | +|---|---| |
| 198 | +| `windowRules.test.ts` | 双向计分(攻略 +60 / 娱乐 −30~−40 对冲)、标题叠加、系统黑名单、置信度分级边界、空标题 | |
| 199 | +| `windowHistory.test.ts` | 标题模板归一化 hash、boost 计算(useCount/recency)、LRU 淘汰、记录写入 | |
| 200 | +| `windowSignals.test.ts` | HWND 解析(含畸形 id)、native 缺失降级(mock)、前台窗口匹配 | |
| 201 | +| `useWindowWatcher.test.ts`(新增) | 自动选中三规则、toast 触发、课程名回填优先级 | |
| 202 | +| `contentClassifier.test.ts`(扩展) | processName 入参分类、游戏进程 + 攻略标题组合 | |
| 203 | + |
| 204 | +验收命令:`npm run lint` + `npm run typecheck` + `npm run typecheck:electron` + `npm run test` 全绿;native 编译通过(`client/native/process-audio` 本地 node-gyp 构建)。 |
| 205 | + |
| 206 | +--- |
| 207 | + |
| 208 | +## 十、文件清单 |
| 209 | + |
| 210 | +**主进程(新增 3 / 改造 3)** |
| 211 | +- 新增 `client/electron/windowSignals.ts` |
| 212 | +- 新增 `client/electron/windowRules.ts` |
| 213 | +- 新增 `client/electron/windowHistory.ts` |
| 214 | +- 改造 `client/electron/windowScorer.ts`(组合入口,导出兼容) |
| 215 | +- 改造 `client/electron/screenCaptureHandlers.ts`(信号注入、监听 diff、新 IPC) |
| 216 | +- 改造 `client/electron/ipc/channels.ts` + `client/electron/preload.ts`(新通道登记) |
| 217 | + |
| 218 | +**原生(改造 2)** |
| 219 | +- 改造 `client/native/process-audio/src/window_finder.cc`(枚举结构加 rect/alwaysOnTop/前台 HWND) |
| 220 | +- 改造 `client/electron/audio/processAudioNative.ts`(类型扩展 + 前台窗口查询导出) |
| 221 | + |
| 222 | +**渲染进程(改造 4 + 测试)** |
| 223 | +- `client/src/lib/capture/captureTypes.ts`(WindowInfo 扩展) |
| 224 | +- `client/src/features/classroom/hooks/useWindowWatcher.ts`(自动选中 + 记忆回填) |
| 225 | +- `client/src/lib/capture/contentClassifier.ts`(processName 信号) |
| 226 | +- `client/src/features/classroom/components/WindowSelectCard.tsx`(reasons 展示 + 自动选中态) |
| 227 | +- 新增 `windowRules.test.ts` / `windowHistory.test.ts` / `windowSignals.test.ts` / `useWindowWatcher.test.ts`;扩展 `contentClassifier.test.ts` |
| 228 | + |
| 229 | +--- |
| 230 | + |
| 231 | +## 十一、阶段拆分(实施顺序,各阶段独立可验证) |
| 232 | + |
| 233 | +1. **阶段一:评分模型重构** — windowRules + windowScorer + signals 纯标题版;行为兼容,全测试绿。 |
| 234 | +2. **阶段二:原生信号接入** — C++ 字段扩展 + 进程/几何/前台信号 + 本地 node-gyp 构建 + 打包验证。 |
| 235 | +3. **阶段三:记忆 + 自动选中** — windowHistory + 新 IPC + useWindowWatcher + WindowSelectCard UI。 |
| 236 | +4. **阶段四:联动与监听** — 课程名回填、contentClassifier 进程信号、标题 diff 升级。 |
| 237 | + |
| 238 | +--- |
| 239 | + |
| 240 | +## 十二、明确不做(YAGNI) |
| 241 | + |
| 242 | +- 缩略图 OCR/视觉后验(本地 OCR 已落地但本次不引入,记为 P2 候选:对 top-3 候选做一次 OCR 提拉首次识别的 medium 候选)。 |
| 243 | +- macOS 系统信号(保持现状标题逻辑)。 |
| 244 | +- 跨平台抽象层(本次仅课堂模块一个消费者)。 |
0 commit comments