feat(#5): авто-пауза напоминаний во время ВКС (Zoom/Teams/Discord/Webex)
This commit is contained in:
97
src/main/meeting-detect.ts
Normal file
97
src/main/meeting-detect.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
/**
|
||||
* Эвристическое обнаружение «человек на ВКС» по списку запущенных процессов.
|
||||
*
|
||||
* Идея: если запущен Zoom/Teams/Discord/Meet/Webex — пользователь скорее
|
||||
* всего на встрече или собирается зайти. Останавливаем напоминания, чтобы
|
||||
* не прерывать. После «снятия» процессов возобновляем.
|
||||
*
|
||||
* Проверка раз в 30 сек через `tasklist` (CSV-режим, без окна). Не используем
|
||||
* audio capture API / mic state — это потребовало бы Windows-specific
|
||||
* native-модуля; задача fit-for-purpose, не security-critical.
|
||||
*
|
||||
* False positive vs false negative tradeoff: лучше один лишний скип
|
||||
* напоминания, чем один pop-up посреди митинга. Поэтому даже если
|
||||
* процесс запущен, но митинга нет (Teams в фоне) — пауза. Пользователь
|
||||
* это переживёт, обратное — не очень.
|
||||
*/
|
||||
import { exec } from 'node:child_process'
|
||||
import { promisify } from 'node:util'
|
||||
import { log } from './logger'
|
||||
|
||||
const execAsync = promisify(exec)
|
||||
|
||||
/**
|
||||
* Имена процессов (Windows .exe). Регистр игнорируется при сравнении.
|
||||
* Покрываем основные VOIP-приложения для индустрии WFH.
|
||||
*/
|
||||
const MEETING_PROCESSES = new Set([
|
||||
'zoom.exe',
|
||||
'teams.exe',
|
||||
'ms-teams.exe', // новые Teams 2.0
|
||||
'discord.exe',
|
||||
'webex.exe',
|
||||
'webexmta.exe',
|
||||
'meet.exe', // Google Meet desktop (редкость)
|
||||
'slack.exe', // huddle
|
||||
'skype.exe',
|
||||
'gotomeeting.exe',
|
||||
'whereby.exe'
|
||||
])
|
||||
|
||||
let cachedActive = false
|
||||
let lastCheckAt = 0
|
||||
const CACHE_MS = 30_000
|
||||
|
||||
/**
|
||||
* Проверяет, запущен ли хотя бы один из meeting-процессов. Кеширует
|
||||
* результат на CACHE_MS, чтобы tasklist не дёргался каждую секунду из
|
||||
* scheduler-tick'а.
|
||||
*/
|
||||
export async function isMeetingActive(): Promise<boolean> {
|
||||
if (process.platform !== 'win32') return false
|
||||
const now = Date.now()
|
||||
if (now - lastCheckAt < CACHE_MS) return cachedActive
|
||||
lastCheckAt = now
|
||||
try {
|
||||
// CSV без заголовков (/NH), скрытое окно.
|
||||
const { stdout } = await execAsync('tasklist /FO CSV /NH', {
|
||||
windowsHide: true,
|
||||
maxBuffer: 4 * 1024 * 1024 // tasklist бывает большой
|
||||
})
|
||||
const lower = stdout.toLowerCase()
|
||||
for (const proc of MEETING_PROCESSES) {
|
||||
// Простой substring-match достаточен: формат CSV-row
|
||||
// "zoom.exe","12345","Console","1","85,432 K".
|
||||
if (lower.includes(`"${proc}",`)) {
|
||||
if (!cachedActive) {
|
||||
log.info(`[meeting] detected ${proc} — pausing reminders`)
|
||||
}
|
||||
cachedActive = true
|
||||
return true
|
||||
}
|
||||
}
|
||||
if (cachedActive) {
|
||||
log.info('[meeting] no meeting processes — resuming reminders')
|
||||
}
|
||||
cachedActive = false
|
||||
return false
|
||||
} catch (e) {
|
||||
// tasklist может фейлиться на нестандартных образах Windows. Лучше
|
||||
// продолжить работу как обычно (не паузить), чем глушить напоминания
|
||||
// навсегда.
|
||||
log.warn('[meeting] tasklist failed, assuming no meeting', e)
|
||||
cachedActive = false
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/** Синхронный «последнее известное значение» без запроса. Используется
|
||||
* в scheduler-tick'е — он не async. Background refresh идёт отдельно. */
|
||||
export function isMeetingActiveSync(): boolean {
|
||||
return cachedActive
|
||||
}
|
||||
|
||||
/** Background refresh, дёргаем из scheduler'а. Не await'им. */
|
||||
export function refreshMeetingState(): void {
|
||||
void isMeetingActive()
|
||||
}
|
||||
Reference in New Issue
Block a user