feat: tier-1 PC tools parity — window mgmt, clipboard, notes, process killer, web search
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.
This commit is contained in:
parent
80a685230c
commit
7d83bf8299
2 changed files with 256 additions and 0 deletions
130
commands.yaml
130
commands.yaml
|
|
@ -277,6 +277,136 @@ read_screen:
|
||||||
- сними текст с экрана
|
- сними текст с экрана
|
||||||
action: {type: ocr_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:
|
thanks:
|
||||||
phrases:
|
phrases:
|
||||||
- спасибо
|
- спасибо
|
||||||
|
|
|
||||||
126
main.py
126
main.py
|
|
@ -395,6 +395,122 @@ def do_file_search(action, voice: str):
|
||||||
_speak(f"Найдено {len(lines)}, открываю первое: {os.path.basename(first)}.")
|
_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):
|
def do_audio_devices_panel(action, voice: str):
|
||||||
# Full programmatic switching needs IPolicyConfig + IMMDeviceEnumerator
|
# Full programmatic switching needs IPolicyConfig + IMMDeviceEnumerator
|
||||||
# via comtypes; out of scope for this pass. Opens Sound control panel so
|
# 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 '')
|
do_file_search(action, _current_voice or '')
|
||||||
elif t == 'audio_devices_panel':
|
elif t == 'audio_devices_panel':
|
||||||
do_audio_devices_panel(action, _current_voice or '')
|
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 = ''
|
_current_voice: str = ''
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue