From 7d83bf829952f29569bfd75ec5eb46ac86c3723d Mon Sep 17 00:00:00 2001 From: Bossiara13 <236771060+DmitryBykov-ISPO@users.noreply.github.com> Date: Fri, 15 May 2026 11:27:19 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20tier-1=20PC=20tools=20parity=20?= =?UTF-8?q?=E2=80=94=20window=20mgmt,=20clipboard,=20notes,=20process=20ki?= =?UTF-8?q?ller,=20web=20search?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirrors the new rust packs from the same iteration. Total commands in commands.yaml is now 49 (was 33). yaml.safe_load + ast.parse both pass. Window management (8 commands) — uses the existing `keys` action with pyautogui hotkey strings: winleft+d (show_desktop), winleft+up/down (maximize/minimize), winleft+left/right (snap left/right), alt+f4 (close_window), winleft+shift+m (restore_all), winleft+tab (task_view). No new action type needed. Clipboard read aloud — new clipboard_read action. Shells to PowerShell Get-Clipboard, caps preview at 400 chars, speaks via the existing tts.va_speak (which already handles recorder.stop/start). Notes — new notes_add + notes_open actions. notes_add strips the trigger phrase, appends "[YYYY-MM-DD HH:MM] body\n" to %USERPROFILE%\Documents\jarvis-notes.txt, creates the dir if needed. notes_open shells "cmd /c start" so .txt opens in the user-default editor. Process killer — new process_kill action. Strips the trigger ("убей процесс"/"закрой программу"/etc.), looks up the remainder in the same alias map as the rust version (хром→chrome, телега→telegram, спотифай →spotify, едж/эдж/edge→msedge, дискорд→discord, стим→steam, обс→obs64, проводник/блокнот/калькулятор/диспетчер задач, vs code→code, etc.), appends .exe if needed, runs taskkill /F /IM, speaks success or "не нашёл процесс N". Web search — new websearch action with a `service` param. Bases: google → https://www.google.com/search?q= youtube → https://www.youtube.com/results?search_query= wiki → https://ru.wikipedia.org/wiki/Special:Search?search= yandex → https://yandex.ru/search/?text= Strips trigger, urllib.parse.quote-s the rest, opens via webbrowser.open (respects the user'\''s default browser; config.BROWSER_PATHS only kicks in for the older url-action commands). Portability: USERPROFILE used for notes path, no other hardcoded paths. --- commands.yaml | 130 ++++++++++++++++++++++++++++++++++++++++++++++++++ main.py | 126 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 256 insertions(+) diff --git a/commands.yaml b/commands.yaml index e7c91fc..73bba1b 100644 --- a/commands.yaml +++ b/commands.yaml @@ -277,6 +277,136 @@ read_screen: - сними текст с экрана action: {type: ocr_screen} +# Window management via pyautogui hotkeys +show_desktop: + phrases: + - покажи рабочий стол + - свернуть всё + - сверни все окна + - покажи десктоп + action: {type: keys, sequence: 'winleft+d'} + +maximize_window: + phrases: + - разверни окно + - развернуть окно + - на весь экран + - раскрой окно + action: {type: keys, sequence: 'winleft+up'} + +minimize_window: + phrases: + - сверни окно + - свернуть окно + - убери окно + action: {type: keys, sequence: 'winleft+down'} + +snap_left: + phrases: + - окно влево + - сдвинь влево + - прижми влево + action: {type: keys, sequence: 'winleft+left'} + +snap_right: + phrases: + - окно вправо + - сдвинь вправо + - прижми вправо + action: {type: keys, sequence: 'winleft+right'} + +close_window: + phrases: + - закрой окно + - закрой вкладку + - закрой текущее + action: {type: keys, sequence: 'alt+f4'} + +restore_all: + phrases: + - восстанови окна + - верни окна + - разверни окна + action: {type: keys, sequence: 'winleft+shift+m'} + +task_view: + phrases: + - открой все окна + - обзор окон + - таск вью + - покажи все окна + action: {type: keys, sequence: 'winleft+tab'} + +# Clipboard read aloud +read_clipboard: + phrases: + - прочитай буфер + - что в буфере + - прочитай из буфера + - озвучь буфер + action: {type: clipboard_read} + +# Notes +add_note: + phrases: + - запиши + - запиши заметку + - запиши идею + - запомни + - запомни идею + - сделай заметку + action: {type: notes_add} + +open_notes: + phrases: + - покажи заметки + - открой заметки + - что я записал + action: {type: notes_open} + +# Process killer +kill_process: + phrases: + - убей процесс + - закрой программу + - завершить процесс + - выруби процесс + action: {type: process_kill} + +# Web search shortcuts +search_google: + phrases: + - загугли + - поиск в гугл + - поищи в гугл + - найди в гугл + - ищи в гугл + action: {type: websearch, service: google} + +search_youtube: + phrases: + - найди на ютубе + - поиск на ютубе + - ищи на ютубе + - ютуб поиск + action: {type: websearch, service: youtube} + +search_wiki: + phrases: + - найди в википедии + - поиск в википедии + - википедия + - вики поиск + action: {type: websearch, service: wiki} + +search_yandex: + phrases: + - поищи в яндекс + - найди в яндекс + - яндекс поиск + - пробей в яндекс + action: {type: websearch, service: yandex} + thanks: phrases: - спасибо diff --git a/main.py b/main.py index 09dd4dc..865c359 100644 --- a/main.py +++ b/main.py @@ -395,6 +395,122 @@ def do_file_search(action, voice: str): _speak(f"Найдено {len(lines)}, открываю первое: {os.path.basename(first)}.") +_NOTES_TRIGGERS = ( + 'запиши заметку', 'запиши идею', 'сделай заметку', 'запомни идею', + 'запиши', 'запомни', +) + +_PROCESS_KILL_TRIGGERS = ( + 'завершить процесс', 'выруби процесс', 'убей процесс', 'kill процесс', + 'закрой программу', +) + +_WEBSEARCH_TRIGGERS = ( + 'загугли', 'поиск в гугл', 'поищи в гугл', 'найди в гугл', 'ищи в гугл', + 'найди на ютубе', 'поиск на ютубе', 'ищи на ютубе', 'ютуб поиск', + 'найди в википедии', 'поиск в википедии', 'википедия', 'вики поиск', + 'поищи в яндекс', 'найди в яндекс', 'яндекс поиск', 'пробей в яндекс', +) + +_PROCESS_ALIASES = { + 'хром': 'chrome', 'хрома': 'chrome', + 'спотифай': 'spotify', + 'едж': 'msedge', 'эдж': 'msedge', 'edge': 'msedge', + 'файрфокс': 'firefox', 'фаерфокс': 'firefox', + 'вскод': 'code', 'vs code': 'code', + 'дискорд': 'discord', + 'телега': 'telegram', 'телеграм': 'telegram', + 'стим': 'steam', + 'обс': 'obs64', + 'проводник': 'explorer', + 'блокнот': 'notepad', + 'калькулятор': 'calculatorapp', + 'диспетчер задач': 'taskmgr', +} + +_WEBSEARCH_BASES = { + 'google': 'https://www.google.com/search?q=', + 'youtube': 'https://www.youtube.com/results?search_query=', + 'wiki': 'https://ru.wikipedia.org/wiki/Special:Search?search=', + 'yandex': 'https://yandex.ru/search/?text=', +} + + +def do_clipboard_read(action, voice: str): + res = subprocess.run( + ['powershell', '-NoProfile', '-Command', 'Get-Clipboard'], + capture_output=True, text=True, encoding='utf-8', timeout=10, + ) + text = (res.stdout or '').strip() + if not text: + _speak("Буфер пуст.") + return + to_speak = text[:400] + if len(text) > 400: + to_speak += " ... и так далее" + _speak(to_speak.replace('\r', ' ').replace('\n', ' ')) + + +def do_notes_add(action, voice: str): + body = _strip_trigger(voice, _NOTES_TRIGGERS).lstrip(' ,.:').strip() + if not body: + _speak("Что записать, сэр?") + return + user_profile = os.environ.get('USERPROFILE', os.path.expanduser('~')) + notes_path = os.path.join(user_profile, 'Documents', 'jarvis-notes.txt') + stamp = datetime.datetime.now().strftime('%Y-%m-%d %H:%M') + line = f"[{stamp}] {body}\n" + try: + os.makedirs(os.path.dirname(notes_path), exist_ok=True) + with open(notes_path, 'a', encoding='utf-8') as f: + f.write(line) + except OSError as e: + print(f"notes append failed: {e}") + _speak("Не удалось записать.") + return + _speak(f"Записал: {body[:120]}") + + +def do_notes_open(action, voice: str): + user_profile = os.environ.get('USERPROFILE', os.path.expanduser('~')) + notes_path = os.path.join(user_profile, 'Documents', 'jarvis-notes.txt') + if not os.path.isfile(notes_path): + _speak("Заметок пока нет.") + return + subprocess.Popen(['cmd', '/c', 'start', '', notes_path], shell=False) + _speak("Открываю заметки.") + + +def do_process_kill(action, voice: str): + name = _strip_trigger(voice, _PROCESS_KILL_TRIGGERS).strip().lower() + target = _PROCESS_ALIASES.get(name, name) + if not target: + _speak("Какой процесс?") + return + exe = target if target.endswith('.exe') else target + '.exe' + res = subprocess.run( + ['taskkill', '/F', '/IM', exe], + capture_output=True, text=True, timeout=10, + ) + if res.returncode == 0: + _speak(f"Закрыл {exe}.") + else: + _speak(f"Не нашёл процесс {exe}.") + + +def do_websearch(action, voice: str): + import urllib.parse + svc = action.get('service', 'google') + base = _WEBSEARCH_BASES.get(svc, _WEBSEARCH_BASES['google']) + query = _strip_trigger(voice, _WEBSEARCH_TRIGGERS).strip() + if not query: + _speak(f"Что искать в {svc}?") + return + url = base + urllib.parse.quote(query) + webbrowser.open(url) + _speak(f"Открываю {svc}: {query[:100]}") + + def do_audio_devices_panel(action, voice: str): # Full programmatic switching needs IPolicyConfig + IMMDeviceEnumerator # via comtypes; out of scope for this pass. Opens Sound control panel so @@ -489,6 +605,16 @@ def run_action(action): do_file_search(action, _current_voice or '') elif t == 'audio_devices_panel': do_audio_devices_panel(action, _current_voice or '') + elif t == 'clipboard_read': + do_clipboard_read(action, _current_voice or '') + elif t == 'notes_add': + do_notes_add(action, _current_voice or '') + elif t == 'notes_open': + do_notes_open(action, _current_voice or '') + elif t == 'process_kill': + do_process_kill(action, _current_voice or '') + elif t == 'websearch': + do_websearch(action, _current_voice or '') _current_voice: str = ''