Skip to content

Commit b7c02b0

Browse files
committed
feat(window-recognition): UI理由展示+内容分类进程信号+监听diff含标题变化
1 parent c237f7d commit b7c02b0

5 files changed

Lines changed: 43 additions & 15 deletions

File tree

‎client/electron/screenCaptureHandlers.ts‎

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,8 @@ let captureSessionToken = 0;
5454
/** 窗口监听轮询定时器 */
5555
let windowWatchTimer: ReturnType<typeof setInterval> | null = null;
5656

57-
/** 上一次窗口列表的 id 集合(用于 diff 检测变化) */
58-
let lastWindowIds: Set<string> = new Set();
57+
/** 上一次窗口列表的 id|name 键集合(用于 diff 检测变化,标题变化也触发) */
58+
let lastWindowKeys: Set<string> = new Set();
5959

6060
const WINDOW_WATCH_INTERVAL_MS = 3000;
6161

@@ -292,15 +292,15 @@ export function registerScreenCaptureHandlers(): void {
292292
thumbnailSize: { width: 240, height: 135 },
293293
});
294294

295-
const currentIds = new Set(sources.map((s) => s.id));
295+
const currentKeys = new Set(sources.map((s) => `${s.id}|${s.name}`));
296296

297-
// 检测是否有变化(新增或关闭窗口)
297+
// 检测是否有变化(新增/关闭窗口,或窗口标题变化)
298298
const hasChanged =
299-
currentIds.size !== lastWindowIds.size ||
300-
[...currentIds].some((id) => !lastWindowIds.has(id));
299+
currentKeys.size !== lastWindowKeys.size ||
300+
[...currentKeys].some((k) => !lastWindowKeys.has(k));
301301

302302
if (hasChanged) {
303-
lastWindowIds = currentIds;
303+
lastWindowKeys = currentKeys;
304304

305305
const rawWindows = sources.map((src) => {
306306
const thumb = src.thumbnail.isEmpty() ? src.thumbnail : src.thumbnail.resize({ width: 120 });
@@ -340,7 +340,7 @@ export function registerScreenCaptureHandlers(): void {
340340
if (windowWatchTimer) {
341341
clearInterval(windowWatchTimer);
342342
windowWatchTimer = null;
343-
lastWindowIds = new Set();
343+
lastWindowKeys = new Set();
344344
logger.info('[IPC] 窗口监听已停止');
345345
}
346346
return { success: true };
@@ -385,7 +385,7 @@ export function disposeScreenCaptureHandlers(): void {
385385
if (windowWatchTimer) {
386386
clearInterval(windowWatchTimer);
387387
windowWatchTimer = null;
388-
lastWindowIds = new Set();
388+
lastWindowKeys = new Set();
389389
}
390390
if (activeCapture) {
391391
activeCapture.dispose();

‎client/src/features/classroom/components/WindowSelectCard.tsx‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,8 @@ function WindowRow({ win, isSelected, onClick }: { win: WindowInfo; isSelected:
4141
)}
4242
<div className="flex-1 min-w-0">
4343
<span className="text-b3 leading-tight line-clamp-1 block">{win.title}</span>
44-
{win.matched && (
45-
<span className="text-[10px] text-brand-500 leading-tight">匹配:{win.matched}</span>
44+
{win.reasons?.[0] && (
45+
<span className="text-[10px] text-brand-500 leading-tight">推荐:{win.reasons[0]}</span>
4646
)}
4747
</div>
4848
{(win.score ?? 0) >= 100 && (
@@ -115,6 +115,9 @@ export function WindowSelectCard({ windows, selected, onSelect, onRefresh, loadi
115115
{selected.matched && (
116116
<span className="text-[10px] text-brand-500 leading-tight">匹配:{selected.matched}</span>
117117
)}
118+
{selected.confidence === 'high' && (
119+
<span className="text-[10px] font-medium text-brand-600 flex-shrink-0">自动识别</span>
120+
)}
118121
</div>
119122
<span className="text-[11px] text-brand-600 flex-shrink-0">更换</span>
120123
</>

‎client/src/features/classroom/hooks/useWindowWatcher.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import type { WindowInfo, CourseMeta } from '@/lib/capture';
1313
const WINDOW_MISSING_THRESHOLD = 10;
1414

1515
/** 常见课程名关键词(规则模式兜底提取) */
16-
const COURSE_KEYWORDS = /((?:高等数学|线性代数|概率论|大学物理|数据结构|操作系统|编译原理|离散数学|复变函数|英语|高数|大物|C语言|Python|Java|机器学习|深度学习|人工智能|计算机网络|数据库)[^\s|]*)/;
16+
const COURSE_KEYWORDS = /((?:高等数学|线性代数|概率论|大学物理|数据结构|操作系统|编译原理|离散数学|复变函数|英语|高数|大物|C语言|Python|Java|机器学习|深度学习|人工智能|计算机网络|数据库|入门|进阶|实战|从零|攻略|教程)[^\s|]*)/;
1717

1818
interface UseWindowWatcherOptions {
1919
courseMeta: CourseMeta;

‎client/src/lib/capture/contentClassifier.test.ts‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,21 @@ describe('classifyByTitle — 窗口标题规则', () => {
4343
});
4444
});
4545

46+
describe('classifyByTitle — 进程名信号', () => {
47+
it('进程名命中软件名单 → software_skill(标题无特征词)', () => {
48+
expect(classifyByTitle('随便看看', 'photoshop.exe')).toBe('software_skill');
49+
expect(classifyByTitle('随便看看', 'obs64.exe')).toBe('software_skill');
50+
expect(classifyByTitle('随便看看', 'code.exe')).toBe('software_skill');
51+
});
52+
it('游戏/直播进程不误判,交给标题判定', () => {
53+
expect(classifyByTitle('随便看看', 'steam.exe')).toBe('unknown');
54+
expect(classifyByTitle('只狼全 Boss 打法教学', 'steam.exe')).not.toBe('unknown');
55+
});
56+
it('非软件进程保持原行为', () => {
57+
expect(classifyByTitle('高等数学全程课程', 'chrome.exe')).toBe('course');
58+
});
59+
});
60+
4661
describe('classifyByTranscript — 转写证据投票', () => {
4762
it('软件操作指令词显著 → software_skill', () => {
4863
expect(classifyByTranscript('我们点击图层面板,把这个参数调整一下,快捷键是 Ctrl+J')).toBe('software_skill');

‎client/src/lib/capture/contentClassifier.ts‎

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ const CRAFT_TITLE_RE =
3232
const LECTURE_TITLE_RE =
3333
/讲座|演讲|发布会|报告|论坛|会议|访谈|大师课|公开课/i;
3434

35+
/** 软件技能进程名单(标题无特征词时,进程名兜底判定;Windows) */
36+
const SOFTWARE_PROCESS_RE =
37+
/photoshop|premiere|afterfx|illustrator|lightroom|剪映|obs64|blender|figma|sketch|unity|unreal|code\.exe|pycharm|intellij|webstorm|notepad\+\+|wps\.exe/i;
38+
3539
// ================================================================
3640
// 转写术语证据规则
3741
// ================================================================
@@ -59,13 +63,15 @@ const COURSE_CUES = [
5963
// ================================================================
6064

6165
/** 窗口标题单独分类(无转写信号时) */
62-
export function classifyByTitle(title: string): ContentKind {
66+
export function classifyByTitle(title: string, processName?: string): ContentKind {
6367
const t = (title ?? '').trim();
6468
if (!t) return 'unknown';
6569
// 讲座/会议类优先("AI 开发者大会"含"开发"字样但属讲座场景,需先于软件判定)
6670
if (LECTURE_TITLE_RE.test(t)) return 'lecture';
6771
if (SOFTWARE_TITLE_RE.test(t)) return 'software_skill';
6872
if (CRAFT_TITLE_RE.test(t)) return 'craft_skill';
73+
// 进程名兜底:软件技能进程在标题无特征词时判为软件技能
74+
if (processName && SOFTWARE_PROCESS_RE.test(processName)) return 'software_skill';
6975
// 标题含"教程/课程/课堂/教学"默认按知识授课(视频站标题高频形态)
7076
if (/教程|课程|课堂|教学|网课/i.test(t)) return 'course';
7177
return 'unknown';
@@ -100,8 +106,12 @@ export interface ClassificationResult {
100106
* 综合分类:标题优先,无标题信号时用转写证据。
101107
* 标题信号覆盖转写(窗口标题是用户主动选择的上下文,可信度更高)。
102108
*/
103-
export function classifyContent(windowTitle: string, transcriptText: string): ClassificationResult {
104-
const byTitle = classifyByTitle(windowTitle);
109+
export function classifyContent(
110+
windowTitle: string,
111+
transcriptText: string,
112+
processName?: string,
113+
): ClassificationResult {
114+
const byTitle = classifyByTitle(windowTitle, processName);
105115
if (byTitle !== 'unknown') return { kind: byTitle, source: 'title' };
106116
const byTranscript = classifyByTranscript(transcriptText);
107117
if (byTranscript !== 'unknown') return { kind: byTranscript, source: 'transcript' };

0 commit comments

Comments
 (0)