feat: parity — games, mouse, random_choice + new packs (90 commands)

Mirrors the rust feature/pc-tools work. yaml.safe_load + ast.parse pass.
82 → 90 commands (+8).

main.py:
+ do_launch_game / do_list_games: reads ~/Documents/jarvis-games.json
  (creates sample on first call with dota / cs2 / elden ring / witcher
  3 / factorio entries). Fuzzy name+alias match, launches via
  steam://rungameid/<appid> (webbrowser.open), epic_uri, or
  cmd /c start "" path.
+ do_mouse: action param op = left|right|middle|double|scroll_up|
  scroll_down. Embedded PowerShell P/Invokes user32!mouse_event with
  the right flag pairs.
+ do_random_choice: strips trigger, splits on " или " / " or " / ", " /
  " либо " / " чи ", picks one with random.choice.

commands.yaml +8 entries:
+ launch_game / list_games
+ mouse_left_click / mouse_right_click / mouse_double_click /
  mouse_scroll_up / mouse_scroll_down (all action:mouse, op:...)
+ random_choice
This commit is contained in:
Bossiara13 2026-05-15 12:58:32 +03:00
parent 40a46567b8
commit b3a5bcabe5
2 changed files with 186 additions and 0 deletions

View file

@ -681,6 +681,65 @@ help:
- что можешь
action: {type: help_commands}
launch_game:
phrases:
- запусти игру
- включи игру
- поиграем
- хочу поиграть
- запусти доту
- включи кс
- включи фортнайт
action: {type: launch_game}
list_games:
phrases:
- какие у меня игры
- список игр
- покажи игры
action: {type: list_games}
mouse_left_click:
phrases:
- клик
- кликни
- нажми мышкой
action: {type: mouse, op: left}
mouse_right_click:
phrases:
- правый клик
- клик правой
- правой кнопкой
action: {type: mouse, op: right}
mouse_double_click:
phrases:
- двойной клик
- дабл клик
- двойное нажатие
action: {type: mouse, op: double}
mouse_scroll_down:
phrases:
- промотай вниз
- прокрути вниз
action: {type: mouse, op: scroll_down}
mouse_scroll_up:
phrases:
- промотай вверх
- прокрути вверх
action: {type: mouse, op: scroll_up}
random_choice:
phrases:
- выбери из
- выбери
- решай за меня
- выбрать
action: {type: random_choice}
thanks:
phrases:
- спасибо

127
main.py
View file

@ -1074,6 +1074,125 @@ def do_help_commands(action, voice: str):
_speak(f"Загружено {len(names)} команд. Например: {preview}. Полный список в буфере.")
_GAME_TRIGGERS = ('запусти игру', 'включи игру', 'хочу поиграть', 'поиграем',
'запусти', 'включи', 'launch game', 'play game', 'launch',
'запусти гру', 'увімкни гру')
def _load_games_config():
user_profile = os.environ.get('USERPROFILE', os.path.expanduser('~'))
cfg_path = os.path.join(user_profile, 'Documents', 'jarvis-games.json')
if not os.path.isfile(cfg_path):
sample = [
{"name": "dota", "aliases": ["доту", "доту 2", "dota 2"], "steam_appid": 570},
{"name": "cs2", "aliases": ["кс", "контру", "контр страйк"], "steam_appid": 730},
{"name": "elden ring", "aliases": ["элден", "элден ринг"], "steam_appid": 1245620},
{"name": "witcher 3", "aliases": ["ведьмак", "ведьмака 3"], "steam_appid": 292030},
{"name": "factorio", "aliases": ["факторио"], "steam_appid": 427520},
]
os.makedirs(os.path.dirname(cfg_path), exist_ok=True)
with open(cfg_path, 'w', encoding='utf-8') as f:
json.dump(sample, f, ensure_ascii=False, indent=2)
return cfg_path, sample, True
try:
with open(cfg_path, 'r', encoding='utf-8') as f:
return cfg_path, json.load(f), False
except (OSError, json.JSONDecodeError) as e:
print(f"games config read failed: {e}")
return cfg_path, [], False
def do_launch_game(action, voice: str):
cfg_path, entries, created = _load_games_config()
if created:
_speak("Создал шаблон, добавь свои игры и повтори.")
subprocess.Popen(['cmd', '/c', 'start', '', cfg_path])
return
query = _strip_trigger(voice, _GAME_TRIGGERS).strip().lower()
if not query:
_speak("Какую игру?")
return
def name_match(e, q):
if e['name'].lower() == q: return True
if q in e['name'].lower(): return True
for a in e.get('aliases', []):
if a.lower() == q or a.lower() in q: return True
return False
match = next((e for e in entries if name_match(e, query)), None)
if not match:
_speak(f"Не нашёл {query}.")
return
appid = match.get('steam_appid')
path = match.get('path')
epic_uri = match.get('epic_uri')
if appid:
webbrowser.open(f"steam://rungameid/{appid}")
elif epic_uri:
webbrowser.open(epic_uri)
elif path and os.path.isfile(path):
subprocess.Popen(['cmd', '/c', 'start', '', path])
else:
_speak(f"Не настроен запуск для {match['name']}.")
return
_speak(f"Запускаю {match['name']}.")
def do_list_games(action, voice: str):
cfg_path, entries, created = _load_games_config()
if created or not entries:
_speak("Список пуст.")
return
names = [e['name'] for e in entries]
_speak(f"В библиотеке {len(names)}: {', '.join(names)}.")
_MOUSE_PS = r"""
Add-Type -Name MouseM -Namespace Win32 -MemberDefinition @'
[DllImport("user32.dll")]
public static extern void mouse_event(uint flags, int dx, int dy, int data, System.UIntPtr extra);
'@
$LD=0x0002; $LU=0x0004; $RD=0x0008; $RU=0x0010; $MD=0x0020; $MU=0x0040; $WHEEL=0x0800
function Click([uint32]$d,[uint32]$u){ [Win32.MouseM]::mouse_event($d,0,0,0,[UIntPtr]::Zero); Start-Sleep -Milliseconds 30; [Win32.MouseM]::mouse_event($u,0,0,0,[UIntPtr]::Zero) }
switch ($args[0]) {
'left' { Click $LD $LU }
'right' { Click $RD $RU }
'middle' { Click $MD $MU }
'double' { Click $LD $LU; Start-Sleep -Milliseconds 50; Click $LD $LU }
'scroll_up' { [Win32.MouseM]::mouse_event($WHEEL,0,0, 360,[UIntPtr]::Zero) }
'scroll_down' { [Win32.MouseM]::mouse_event($WHEEL,0,0,-360,[UIntPtr]::Zero) }
}
""".strip()
def do_mouse(action, voice: str):
op = action.get('op', 'left')
subprocess.run(
['powershell', '-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', _MOUSE_PS, op],
capture_output=True, timeout=5,
)
_CHOICE_TRIGGERS = ('выбери из', 'выбери', 'решай за меня', 'выбрать',
'pick from', 'choose from', 'decide between',
'обери з', 'вибери')
def do_random_choice(action, voice: str):
rest = _strip_trigger(voice, _CHOICE_TRIGGERS).strip().lower()
options = None
for sep in (' или ', ' or ', ', ', ' либо ', ' чи '):
if sep in rest:
options = [t.strip() for t in rest.split(sep) if t.strip()]
break
if not options or len(options) < 2:
_speak("Скажи: выбери из X или Y или Z.")
return
_speak(f"Я выбираю {random.choice(options)}.")
def do_stopwatch(action, voice: str):
op = action.get('op', 'check')
state = _stopwatch_state_path()
@ -1238,6 +1357,14 @@ def run_action(action):
do_power(action, _current_voice or '')
elif t == 'help_commands':
do_help_commands(action, _current_voice or '')
elif t == 'launch_game':
do_launch_game(action, _current_voice or '')
elif t == 'list_games':
do_list_games(action, _current_voice or '')
elif t == 'mouse':
do_mouse(action, _current_voice or '')
elif t == 'random_choice':
do_random_choice(action, _current_voice or '')
_current_voice: str = ''