From 41eb47724cd39bedb615ae99cfb01bc071dc4fba Mon Sep 17 00:00:00 2001 From: Bossiara13 <236771060+DmitryBykov-ISPO@users.noreply.github.com> Date: Fri, 15 May 2026 18:32:13 +0300 Subject: [PATCH] feat: lock_workstation + screenshot + net_info packs + modernise power pack MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three handy daily-driver packs + cleanup of the oldest power/act.lua. power pack — added lock_workstation (resources/commands/power/) - New command id: lock_workstation - Phrase: "заблокируй компьютер" / "заблокируй пк" / "заблокируй экран" - Action: rundll32.exe user32.dll,LockWorkStation - Sandbox: full (needs system.exec) - act.lua modernised: * No more inline PowerShell SAPI escape chain — now uses jarvis.speak (TTS backend abstraction respects user's chosen Piper/SAPI/Silero). * Returns via jarvis.cmd.ok/error helpers. * Logged warning for failing cmds without crashing the pack. * Cleaner per-action `warn` flag controls the "say cancel to abort" hint. screenshot pack (NEW, resources/commands/screenshot/, 2 commands) - "сделай скрин" / "сделай скриншот" → screenshot.to_clipboard Uses [System.Windows.Forms.Clipboard]::SetImage() with a captured Bitmap. No file artifact, no LLM call — pure capture, fastest path. - "сохрани скрин" → screenshot.to_file Saves to ~/Pictures/Screenshots/jarvis-YYYYMMDD-HHMMSS.png. Creates dir if missing. Notifies with full path. - Distinct from vision/ pack which captures + describes via LLM. net_info pack (NEW, resources/commands/net_info/, 3 commands) - "какая сеть" / "какой wifi" → net.wifi_ssid Parses `netsh wlan show interfaces` for SSID line (RU + EN field names). - "мой IP" / "локальный IP" → net.local_ip PowerShell Get-NetIPAddress, filters out 127.* and 169.* (link-local). - "внешний IP" / "публичный IP" → net.public_ip Hits api.ipify.org via jarvis.http.get (no key, plain text). Tests: 99/99 pass (commands::tests auto-validates new packs). Build: cargo build --release -p jarvis-app green. Pack count: 59 → 62 (intro/echo from prior commit + screenshot + net_info; power already counted — added a command, not a pack). --- resources/commands/net_info/command.toml | 56 ++++++++++++++++ resources/commands/net_info/local_ip.lua | 18 +++++ resources/commands/net_info/public_ip.lua | 12 ++++ resources/commands/net_info/wifi.lua | 18 +++++ resources/commands/power/act.lua | 65 ++++++++++++------- resources/commands/screenshot/command.toml | 38 +++++++++++ .../commands/screenshot/to_clipboard.lua | 25 +++++++ resources/commands/screenshot/to_file.lua | 36 ++++++++++ 8 files changed, 243 insertions(+), 25 deletions(-) create mode 100644 resources/commands/net_info/command.toml create mode 100644 resources/commands/net_info/local_ip.lua create mode 100644 resources/commands/net_info/public_ip.lua create mode 100644 resources/commands/net_info/wifi.lua create mode 100644 resources/commands/screenshot/command.toml create mode 100644 resources/commands/screenshot/to_clipboard.lua create mode 100644 resources/commands/screenshot/to_file.lua diff --git a/resources/commands/net_info/command.toml b/resources/commands/net_info/command.toml new file mode 100644 index 0000000..4c77f99 --- /dev/null +++ b/resources/commands/net_info/command.toml @@ -0,0 +1,56 @@ +# Network info — WiFi SSID, local IP, public IP via PowerShell + free service. +# Separate from existing `network` pack which likely covers different things. + +[[commands]] +id = "net.wifi_ssid" +type = "lua" +script = "wifi.lua" +sandbox = "full" +timeout = 5000 + +[commands.phrases] +ru = [ + "к какой сети я подключен", + "какой wi-fi", + "какой вайфай", + "какая сеть", + "имя сети", +] +en = ["what wifi", "what network", "wifi name", "ssid"] +ua = ["до якої мережі"] + + +[[commands]] +id = "net.local_ip" +type = "lua" +script = "local_ip.lua" +sandbox = "full" +timeout = 5000 + +[commands.phrases] +ru = [ + "мой локальный ip", + "мой ip", + "айпи адрес", + "локальный адрес", +] +en = ["my ip", "local ip", "ip address"] +ua = ["моя айпі", "локальна адреса"] + + +[[commands]] +id = "net.public_ip" +type = "lua" +script = "public_ip.lua" +sandbox = "full" +timeout = 10000 + +[commands.phrases] +ru = [ + "мой внешний ip", + "публичный ip", + "внешний айпи", + "какой у меня внешний", +] +en = ["public ip", "external ip", "what's my ip online"] +ua = ["зовнішня айпі"] diff --git a/resources/commands/net_info/local_ip.lua b/resources/commands/net_info/local_ip.lua new file mode 100644 index 0000000..55c0b50 --- /dev/null +++ b/resources/commands/net_info/local_ip.lua @@ -0,0 +1,18 @@ +-- Get first non-loopback IPv4 address via PowerShell Get-NetIPAddress. +local ps = "(Get-NetIPAddress -AddressFamily IPv4 | " .. + "Where-Object {$_.IPAddress -notlike '127.*' -and $_.IPAddress -notlike '169.*'} | " .. + "Select-Object -First 1).IPAddress" +local res = jarvis.system.exec(string.format( + 'powershell -NoProfile -Command "%s"', ps +)) + +if not res.success then + return jarvis.cmd.error("Не получилось узнать IP.") +end + +local ip = (res.stdout or ""):gsub("%s+", "") +if ip == "" then + return jarvis.cmd.not_found("Нет активного сетевого подключения.") +end + +return jarvis.cmd.ok("Локальный адрес: " .. ip .. ".") diff --git a/resources/commands/net_info/public_ip.lua b/resources/commands/net_info/public_ip.lua new file mode 100644 index 0000000..74968c2 --- /dev/null +++ b/resources/commands/net_info/public_ip.lua @@ -0,0 +1,12 @@ +-- Free public-IP services: api.ipify.org → plain text body. +local r = jarvis.http.get("https://api.ipify.org") +if not r or not r.ok then + return jarvis.cmd.error("Не получилось узнать внешний IP.") +end + +local ip = (r.body or ""):gsub("%s+", "") +if ip == "" then + return jarvis.cmd.error("Сервис вернул пустой ответ.") +end + +return jarvis.cmd.ok("Внешний адрес: " .. ip .. ".") diff --git a/resources/commands/net_info/wifi.lua b/resources/commands/net_info/wifi.lua new file mode 100644 index 0000000..a708fd1 --- /dev/null +++ b/resources/commands/net_info/wifi.lua @@ -0,0 +1,18 @@ +-- "Какая сеть" → `netsh wlan show interfaces` → grep SSID +local res = jarvis.system.exec('netsh wlan show interfaces') +if not res.success then + return jarvis.cmd.error("Не получилось получить данные о Wi-Fi.") +end + +local out = res.stdout or "" +local ssid = out:match("SSID%s+:%s+(.-)\r?\n") +-- Russian Windows: "SSID : MyNetwork" +-- The non-localised case: same format +if not ssid then ssid = out:match("Имя сети%s+:%s+(.-)\r?\n") end +ssid = ssid and ssid:gsub("^%s+", ""):gsub("%s+$", "") or "" + +if ssid == "" or ssid == "(нет)" or ssid == "(none)" then + return jarvis.cmd.not_found("Wi-Fi не подключен или адаптер выключен.") +end + +return jarvis.cmd.ok("Сеть: " .. ssid .. ".") diff --git a/resources/commands/power/act.lua b/resources/commands/power/act.lua index b189e17..84d6d62 100644 --- a/resources/commands/power/act.lua +++ b/resources/commands/power/act.lua @@ -1,45 +1,60 @@ +-- Power-management dispatcher: shutdown/restart/sleep/hibernate/logoff/cancel/lock. +-- All commands share this script; behaviour selected by jarvis.context.command_id. +-- Modernised to use jarvis.speak (TTS backend abstraction) + jarvis.cmd helpers. + local cmd_id = jarvis.context.command_id local lang = jarvis.context.language local actions = { - shutdown_pc = { cmd = 'shutdown.exe /s /t 30 /c "J.A.R.V.I.S.: shutdown in 30s. Say cancel to abort."', label = lang == "ru" and "Выключение через 30 секунд" or "Shutdown in 30 sec" }, - restart_pc = { cmd = 'shutdown.exe /r /t 30 /c "J.A.R.V.I.S.: restart in 30s. Say cancel to abort."', label = lang == "ru" and "Перезагрузка через 30 секунд" or "Restart in 30 sec" }, - logoff_user = { cmd = 'shutdown.exe /l', label = lang == "ru" and "Выход из системы" or "Sign out" }, - sleep_pc = { cmd = 'rundll32.exe powrprof.dll,SetSuspendState 0,1,0', label = lang == "ru" and "Сон" or "Sleep" }, - hibernate_pc = { cmd = 'shutdown.exe /h', label = lang == "ru" and "Гибернация" or "Hibernate" }, - cancel_shutdown = { cmd = 'shutdown.exe /a', label = lang == "ru" and "Выключение отменено" or "Shutdown cancelled" }, + shutdown_pc = { + cmd = 'shutdown.exe /s /t 30 /c "J.A.R.V.I.S.: shutdown in 30s. Say cancel to abort."', + label = lang == "ru" and "Выключение через 30 секунд" or "Shutdown in 30 sec", + warn = true, + }, + restart_pc = { + cmd = 'shutdown.exe /r /t 30 /c "J.A.R.V.I.S.: restart in 30s. Say cancel to abort."', + label = lang == "ru" and "Перезагрузка через 30 секунд" or "Restart in 30 sec", + warn = true, + }, + logoff_user = { cmd = 'shutdown.exe /l', + label = lang == "ru" and "Выход из системы" or "Sign out" }, + sleep_pc = { cmd = 'rundll32.exe powrprof.dll,SetSuspendState 0,1,0', + label = lang == "ru" and "Сон" or "Sleep" }, + hibernate_pc = { cmd = 'shutdown.exe /h', + label = lang == "ru" and "Гибернация" or "Hibernate" }, + cancel_shutdown = { cmd = 'shutdown.exe /a', + label = lang == "ru" and "Выключение отменено" or "Shutdown cancelled" }, + lock_workstation = { cmd = 'rundll32.exe user32.dll,LockWorkStation', + label = lang == "ru" and "Компьютер заблокирован" or "PC locked" }, } local a = actions[cmd_id] if not a then - jarvis.audio.play_error() - return { chain = false } + return jarvis.cmd.error("Unknown power command.") end local res = jarvis.system.exec(a.cmd) local ok = res.success +-- shutdown /a returns non-zero if there was no pending shutdown — treat as success +-- (idempotent "cancel if any"). if not ok and cmd_id == "cancel_shutdown" then ok = true end -if ok then - jarvis.system.notify("Power", a.label) - jarvis.log("info", "power: " .. cmd_id .. " -> " .. a.label) - local say = a.label .. ". " .. (cmd_id == "shutdown_pc" or cmd_id == "restart_pc" - and (lang == "ru" and "Скажи отмени если передумал, сэр." or "Say cancel to abort, sir.") - or "") - local sapi = say:gsub("'", "''"):gsub("\r?\n", " ") - local ps = string.format( - [[Add-Type -AssemblyName System.Speech; $s = New-Object System.Speech.Synthesis.SpeechSynthesizer; foreach ($v in $s.GetInstalledVoices()) { if ($v.VoiceInfo.Culture.TwoLetterISOLanguageName -eq 'ru') { try { $s.SelectVoice($v.VoiceInfo.Name); break } catch {} } } $s.Speak('%s')]], - sapi - ) - jarvis.system.exec(string.format('powershell -NoProfile -Command "%s"', ps:gsub('"', '\\"'))) - jarvis.audio.play_ok() -else +if not ok then jarvis.log("error", "power " .. cmd_id .. " failed: " .. tostring(res.stderr):sub(1, 200)) - jarvis.system.notify("Power", (lang == "ru" and "Не получилось: " or "Failed: ") .. cmd_id) - jarvis.audio.play_error() + return jarvis.cmd.error((lang == "ru" and "Не получилось: " or "Failed: ") .. cmd_id) end -return { chain = false } +jarvis.system.notify("Power", a.label) +jarvis.log("info", "power: " .. cmd_id .. " -> " .. a.label) + +-- For destructive ops with a 30-second window, give the user the abort hint. +local say = a.label +if a.warn then + say = say .. ". " .. (lang == "ru" and "Скажите отмени если передумали." + or "Say cancel to abort.") +end + +return jarvis.cmd.ok(say) diff --git a/resources/commands/screenshot/command.toml b/resources/commands/screenshot/command.toml new file mode 100644 index 0000000..fc7df0c --- /dev/null +++ b/resources/commands/screenshot/command.toml @@ -0,0 +1,38 @@ +# Quick screenshot pack — capture screen, save somewhere useful. +# Unlike vision/, no LLM call — just the capture. Faster + works offline. + +[[commands]] +id = "screenshot.to_clipboard" +type = "lua" +script = "to_clipboard.lua" +sandbox = "full" +timeout = 5000 + +[commands.phrases] +ru = [ + "сделай скриншот", + "сделай скрин", + "скопируй экран", + "скриншот в буфер", + "захвати экран", +] +en = ["take a screenshot", "screenshot", "capture screen"] +ua = ["зроби скрін", "захопи екран"] + + +[[commands]] +id = "screenshot.to_file" +type = "lua" +script = "to_file.lua" +sandbox = "full" +timeout = 5000 + +[commands.phrases] +ru = [ + "сохрани скриншот", + "сохрани скрин", + "скриншот в файл", + "сохрани экран", +] +en = ["save screenshot", "screenshot to file"] +ua = ["збережи скрін"] diff --git a/resources/commands/screenshot/to_clipboard.lua b/resources/commands/screenshot/to_clipboard.lua new file mode 100644 index 0000000..56d07d7 --- /dev/null +++ b/resources/commands/screenshot/to_clipboard.lua @@ -0,0 +1,25 @@ +-- Capture full screen → put PNG into clipboard via PowerShell. +-- One-liner with System.Drawing + System.Windows.Forms.Clipboard.SetImage. + +local ps = [[ +Add-Type -AssemblyName System.Windows.Forms +Add-Type -AssemblyName System.Drawing +$bounds = [System.Windows.Forms.Screen]::PrimaryScreen.Bounds +$bmp = New-Object System.Drawing.Bitmap $bounds.Width, $bounds.Height +$g = [System.Drawing.Graphics]::FromImage($bmp) +$g.CopyFromScreen($bounds.Location, [System.Drawing.Point]::Empty, $bounds.Size) +[System.Windows.Forms.Clipboard]::SetImage($bmp) +$g.Dispose(); $bmp.Dispose() +]] + +local res = jarvis.system.exec(string.format( + 'powershell -NoProfile -ExecutionPolicy Bypass -Command "%s"', + ps:gsub('"', '\\"'):gsub("\r?\n", "; ") +)) + +if not res.success then + jarvis.log("warn", "screenshot to clipboard failed: " .. tostring(res.stderr):sub(1, 200)) + return jarvis.cmd.error("Не получилось сделать скриншот.") +end + +return jarvis.cmd.ok("Скриншот в буфере.") diff --git a/resources/commands/screenshot/to_file.lua b/resources/commands/screenshot/to_file.lua new file mode 100644 index 0000000..e0266da --- /dev/null +++ b/resources/commands/screenshot/to_file.lua @@ -0,0 +1,36 @@ +-- Capture full screen → save PNG under ~/Pictures/Screenshots/jarvis-YYYYMMDD-HHMMSS.png +local t = jarvis.context.time +local stamp = string.format("%04d%02d%02d-%02d%02d%02d", + t.year, t.month, t.day, t.hour, t.minute, t.second or 0) + +local userprofile = jarvis.system.env("USERPROFILE") or "C:\\Users\\Public" +local dir = userprofile .. "\\Pictures\\Screenshots" +local path = string.format("%s\\jarvis-%s.png", dir, stamp) + +-- Ensure dir exists +jarvis.fs.mkdir(dir) + +local escaped = path:gsub("'", "''") +local ps = string.format([[ +Add-Type -AssemblyName System.Windows.Forms +Add-Type -AssemblyName System.Drawing +$bounds = [System.Windows.Forms.Screen]::PrimaryScreen.Bounds +$bmp = New-Object System.Drawing.Bitmap $bounds.Width, $bounds.Height +$g = [System.Drawing.Graphics]::FromImage($bmp) +$g.CopyFromScreen($bounds.Location, [System.Drawing.Point]::Empty, $bounds.Size) +$bmp.Save('%s', [System.Drawing.Imaging.ImageFormat]::Png) +$g.Dispose(); $bmp.Dispose() +]], escaped) + +local res = jarvis.system.exec(string.format( + 'powershell -NoProfile -ExecutionPolicy Bypass -Command "%s"', + ps:gsub('"', '\\"'):gsub("\r?\n", "; ") +)) + +if not res.success then + jarvis.log("warn", "screenshot to file failed: " .. tostring(res.stderr):sub(1, 200)) + return jarvis.cmd.error("Не получилось сохранить скриншот.") +end + +jarvis.system.notify("Скриншот", path) +return jarvis.cmd.ok("Скриншот сохранён в папку Скриншоты.")