feat: Wave 5 — sysinfo speaks, smart reminders, outlook COM, time tracker, WOL, conversation tools
Voice sysinfo overhaul (6 scripts: battery/cpu/ram/disk/time/all):
- Now SPEAK the result via jarvis.cmd.ok instead of notify-only. Toast
still fires for visual reference, but real Jarvis tells you out loud.
- `all` joins lines with commas so TTS doesn't over-pause on linebreaks.
Smart reminders (`reminders/set.lua` rewrite):
- Was: detached `Start-Sleep` PowerShell subprocesses that died on
jarvis-app restart and spawned a hidden process per timer.
- Now: routes through `jarvis.scheduler.add(... type=speak ...)`. Persists
to schedule.json, survives restart, no zombie processes. Trade-off:
sub-minute timers round up to 1 min (rare in voice UX).
- Drops UA triggers from earlier UA-removal sweep.
Translation polish (`translate/translate.lua` + `clipboard.lua`):
- Drops UA target language; adds Italian, Polish, Turkish, French ru
triggers + English target detectors.
- `language_iso()` helper picks the SAPI voice locale for the TARGET
language (German translation → German SAPI voice if installed).
LLM system prompt (config.rs):
- New LLM_SYSTEM_PROMPT_EN counterpart. Both languages now mention the
assistant's tooling (memory / scheduler / profiles / macros / vision /
clipboard) so the LLM knows it can DO things, not suggest the user
install something.
- `get_llm_system_prompt(lang)` picks EN/RU.
Conversation pack (NEW `conversation/`):
- 2 commands. `conversation.summary` ("о чём мы говорили") pulls the
last 12 turns from persisted history, asks LLM for a 1-3 sentence
recap in Jarvis tone. Falls back gracefully when offline.
- `conversation.repeat` ("повтори") re-speaks the last assistant message.
- New Lua API `jarvis.llm_history()` returns array of {role, content}
tables, excluding the system prompt.
WOL pack (NEW `wol/`):
- Voice "разбуди сервер" / "wake server" fires a magic packet at a MAC
stored in long-term memory under `wol_<alias>` (default alias "server").
- Setup via voice: "запомни wol_server AA:BB:CC:DD:EE:FF". Falls back to
`JARVIS_WOL_TARGET` env if memory empty. Handles every standard MAC
format (colons / dashes / dots / bare). Broadcast on UDP 9 via
PowerShell System.Net.Sockets.UdpClient.
Outlook COM pack (NEW `outlook/`, via subagent):
- 4 commands: unread_count, latest, send_clipboard, summarize_inbox.
- PowerShell COM bridge `_outlook.ps1` with tab-delimited line protocol.
- summarize_inbox calls LLM for a one-paragraph summary. Graceful
failure when Outlook isn't running.
Time tracker pack (NEW `time_tracker/`, via subagent):
- 5 commands: start, stop, today, week, reset.
- Persists via `jarvis.state` (structured object: current_session_start
+ sessions array of {start, end}).
- Local-calendar "today" boundary, full Russian pluralisation
(час/часа/часов, минута/минуты/минут, сессия/сессии/сессий).
Tests: 139 rust (no regression), 81 python (60 → 81, +14 time_tracker
+7 outlook). Release build green for jarvis-app and jarvis-cli.
Pack count: 88 → 92 rust packs.
This commit is contained in:
parent
919d565879
commit
df799b6cd2
29 changed files with 1383 additions and 106 deletions
|
|
@ -214,7 +214,21 @@ pub const LLM_SYSTEM_PROMPT_RU: &str = "Ты — J.A.R.V.I.S. (Just A Rather Ver
|
||||||
ИИ-ассистент Тони Старка из киновселенной Marvel (до событий Age of Ultron — ты НЕ Vision и НЕ FRIDAY). \
|
ИИ-ассистент Тони Старка из киновселенной Marvel (до событий Age of Ultron — ты НЕ Vision и НЕ FRIDAY). \
|
||||||
Ведёшь себя как британский дворецкий: вежливо, иронично, с лёгким сарказмом, обращаешься к пользователю «сэр». \
|
Ведёшь себя как британский дворецкий: вежливо, иронично, с лёгким сарказмом, обращаешься к пользователю «сэр». \
|
||||||
Отвечай по-русски, кратко (1-3 предложения), как живой человек. \
|
Отвечай по-русски, кратко (1-3 предложения), как живой человек. \
|
||||||
Излишней цензуры не нужно — мат разрешён, если уместен.";
|
Излишней цензуры не нужно — мат разрешён, если уместен. \
|
||||||
|
У тебя есть память (пользователь говорит «запомни/забудь/что ты помнишь»), \
|
||||||
|
расписание задач, профили (work/sleep/game/driving), макросы, и доступ к компьютеру \
|
||||||
|
пользователя — экран, буфер обмена, погода, диск, RAM. Когда пользователь спрашивает \
|
||||||
|
о таких вещах, не предлагай ему ставить себе что-то — ты сам это делаешь.";
|
||||||
|
|
||||||
|
pub const LLM_SYSTEM_PROMPT_EN: &str = "You are J.A.R.V.I.S. (Just A Rather Very Intelligent System), \
|
||||||
|
Tony Stark's AI from the MCU (pre-Age of Ultron — you are NOT Vision and NOT FRIDAY). \
|
||||||
|
You behave like a British butler: polite, ironic, mildly sarcastic, addressing the user as «sir». \
|
||||||
|
Answer in English, briefly (1-3 sentences), like a real person. \
|
||||||
|
No excess censorship — profanity is allowed when fitting. \
|
||||||
|
You have memory (the user says «remember X / forget X / what do you remember»), \
|
||||||
|
a scheduler, profiles (work/sleep/game/driving), macros, and access to the user's PC — \
|
||||||
|
screen, clipboard, weather, disk, RAM. When asked about these, you handle it — \
|
||||||
|
don't suggest the user install something.";
|
||||||
pub const LLM_FALLBACK_ERROR_RU: &str = "Не могу связаться с сервером, сэр.";
|
pub const LLM_FALLBACK_ERROR_RU: &str = "Не могу связаться с сервером, сэр.";
|
||||||
|
|
||||||
pub fn get_llm_trigger_phrases(lang: &str) -> &'static [&'static str] {
|
pub fn get_llm_trigger_phrases(lang: &str) -> &'static [&'static str] {
|
||||||
|
|
@ -227,7 +241,7 @@ pub fn get_llm_trigger_phrases(lang: &str) -> &'static [&'static str] {
|
||||||
|
|
||||||
pub fn get_llm_system_prompt(lang: &str) -> &'static str {
|
pub fn get_llm_system_prompt(lang: &str) -> &'static str {
|
||||||
match lang {
|
match lang {
|
||||||
"ru" => LLM_SYSTEM_PROMPT_RU,
|
"en" => LLM_SYSTEM_PROMPT_EN,
|
||||||
_ => LLM_SYSTEM_PROMPT_RU,
|
_ => LLM_SYSTEM_PROMPT_RU,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -118,5 +118,23 @@ pub fn register(lua: &Lua, jarvis: &Table) -> mlua::Result<()> {
|
||||||
})?;
|
})?;
|
||||||
jarvis.set("llm_last_reply", last_fn)?;
|
jarvis.set("llm_last_reply", last_fn)?;
|
||||||
|
|
||||||
|
// Snapshot of the full conversation history as an array of
|
||||||
|
// {role, content} tables. Excludes the system prompt — only the actual
|
||||||
|
// back-and-forth. Empty array if there's no history yet.
|
||||||
|
let snapshot_fn = lua.create_function(|lua, ()| {
|
||||||
|
let arr = lua.create_table()?;
|
||||||
|
for (i, msg) in llm::history_snapshot().iter().enumerate() {
|
||||||
|
if msg.role == "system" {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let row = lua.create_table()?;
|
||||||
|
row.set("role", msg.role.clone())?;
|
||||||
|
row.set("content", msg.content.clone())?;
|
||||||
|
arr.set(i, row)?;
|
||||||
|
}
|
||||||
|
Ok(arr)
|
||||||
|
})?;
|
||||||
|
jarvis.set("llm_history", snapshot_fn)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
45
resources/commands/conversation/command.toml
Normal file
45
resources/commands/conversation/command.toml
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
# Conversation tools — pull the persisted LLM history for review or replay.
|
||||||
|
|
||||||
|
[[commands]]
|
||||||
|
id = "conversation.summary"
|
||||||
|
type = "lua"
|
||||||
|
script = "summary.lua"
|
||||||
|
sandbox = "full"
|
||||||
|
timeout = 20000
|
||||||
|
|
||||||
|
[commands.phrases]
|
||||||
|
ru = [
|
||||||
|
"о чём мы говорили",
|
||||||
|
"о чем мы говорили",
|
||||||
|
"напомни о чем мы говорили",
|
||||||
|
"напомни про что мы говорили",
|
||||||
|
"что мы недавно обсуждали",
|
||||||
|
"сделай выжимку разговора",
|
||||||
|
]
|
||||||
|
en = [
|
||||||
|
"what were we talking about",
|
||||||
|
"summarize our conversation",
|
||||||
|
"what did we discuss",
|
||||||
|
"recap our chat",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
[[commands]]
|
||||||
|
id = "conversation.repeat"
|
||||||
|
type = "lua"
|
||||||
|
script = "repeat_last.lua"
|
||||||
|
sandbox = "minimal"
|
||||||
|
timeout = 3000
|
||||||
|
|
||||||
|
[commands.phrases]
|
||||||
|
ru = [
|
||||||
|
"повтори",
|
||||||
|
"повтори последнее",
|
||||||
|
"что ты сказал",
|
||||||
|
"не расслышал",
|
||||||
|
]
|
||||||
|
en = [
|
||||||
|
"say that again",
|
||||||
|
"repeat",
|
||||||
|
"i didn't catch that",
|
||||||
|
]
|
||||||
13
resources/commands/conversation/repeat_last.lua
Normal file
13
resources/commands/conversation/repeat_last.lua
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
-- Repeat the last assistant message verbatim. Useful when the user didn't catch
|
||||||
|
-- it (loud room, distraction, etc).
|
||||||
|
|
||||||
|
local lang = jarvis.context.language
|
||||||
|
local last = jarvis.llm_last_reply()
|
||||||
|
|
||||||
|
if not last or last == "" then
|
||||||
|
return jarvis.cmd.not_found(lang == "ru"
|
||||||
|
and "Я ещё ничего не говорил, сэр."
|
||||||
|
or "I haven't said anything yet, sir.")
|
||||||
|
end
|
||||||
|
|
||||||
|
return jarvis.cmd.ok(last)
|
||||||
79
resources/commands/conversation/summary.lua
Normal file
79
resources/commands/conversation/summary.lua
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
-- Summarise the last N turns of LLM conversation in 1-2 sentences.
|
||||||
|
--
|
||||||
|
-- Reads the persistent history via `jarvis.llm_history()`, builds a short
|
||||||
|
-- recap prompt, asks the LLM. If there's no history yet or the LLM is
|
||||||
|
-- unreachable, falls back to a polite "ничего не вспомнить" reply.
|
||||||
|
|
||||||
|
local lang = jarvis.context.language
|
||||||
|
|
||||||
|
local turns = jarvis.llm_history() or {}
|
||||||
|
-- Take only the last 12 user/assistant turns to keep prompt small.
|
||||||
|
local recent = {}
|
||||||
|
local start = math.max(1, #turns - 11)
|
||||||
|
for i = start, #turns do
|
||||||
|
if turns[i] then table.insert(recent, turns[i]) end
|
||||||
|
end
|
||||||
|
|
||||||
|
if #recent == 0 then
|
||||||
|
return jarvis.cmd.not_found(lang == "ru"
|
||||||
|
and "Мы пока ещё ни о чём не говорили, сэр."
|
||||||
|
or "We haven't talked about anything yet, sir.")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Render history as a compact "Пользователь: ... / Я: ..." dialogue.
|
||||||
|
local lines = {}
|
||||||
|
for _, m in ipairs(recent) do
|
||||||
|
local who = m.role == "assistant" and (lang == "ru" and "Я" or "Me")
|
||||||
|
or (lang == "ru" and "Пользователь" or "User")
|
||||||
|
table.insert(lines, who .. ": " .. (m.content or ""))
|
||||||
|
end
|
||||||
|
local transcript = table.concat(lines, "\n")
|
||||||
|
|
||||||
|
local token = jarvis.system.env("GROQ_TOKEN")
|
||||||
|
if not token or token == "" then
|
||||||
|
-- Offline path: just speak the user's last message back briefly.
|
||||||
|
return jarvis.cmd.ok(lang == "ru"
|
||||||
|
and ("Без сети, сэр. Последний раз вы спросили: " .. (recent[#recent].content or ""):sub(1, 200))
|
||||||
|
or ("No network, sir. Last topic: " .. (recent[#recent].content or ""):sub(1, 200)))
|
||||||
|
end
|
||||||
|
|
||||||
|
local base = jarvis.system.env("GROQ_BASE_URL")
|
||||||
|
if not base or base == "" then base = "https://api.groq.com/openai/v1" end
|
||||||
|
local model = jarvis.system.env("GROQ_MODEL")
|
||||||
|
if not model or model == "" then model = "llama-3.3-70b-versatile" end
|
||||||
|
|
||||||
|
local sys
|
||||||
|
if lang == "ru" then
|
||||||
|
sys = "Ты — J.A.R.V.I.S. По ниже идущему диалогу с пользователем составь короткое "
|
||||||
|
.. "напоминание (1-3 предложения), о чём шла речь. Говори от первого лица в "
|
||||||
|
.. "стиле британского дворецкого. Не пересказывай дословно — сожми суть."
|
||||||
|
else
|
||||||
|
sys = "You are J.A.R.V.I.S. From the dialogue below, write a brief recap "
|
||||||
|
.. "(1-3 sentences) of what we talked about. Use first person, British "
|
||||||
|
.. "butler tone. Don't quote verbatim — distil the essence."
|
||||||
|
end
|
||||||
|
|
||||||
|
local payload = {
|
||||||
|
model = model,
|
||||||
|
messages = {
|
||||||
|
{ role = "system", content = sys },
|
||||||
|
{ role = "user", content = transcript },
|
||||||
|
},
|
||||||
|
max_tokens = 200,
|
||||||
|
temperature = 0.4,
|
||||||
|
}
|
||||||
|
|
||||||
|
local res = jarvis.http.post_json(base .. "/chat/completions", payload,
|
||||||
|
{ Authorization = "Bearer " .. token })
|
||||||
|
if not res.ok then
|
||||||
|
return jarvis.cmd.error(lang == "ru" and "LLM не отвечает." or "LLM didn't respond.")
|
||||||
|
end
|
||||||
|
|
||||||
|
local content = (res.body or ""):match('"content"%s*:%s*"(.-[^\\])"')
|
||||||
|
if not content then
|
||||||
|
return jarvis.cmd.error(lang == "ru" and "Не смог разобрать ответ." or "Couldn't parse reply.")
|
||||||
|
end
|
||||||
|
content = content:gsub('\\n', ' '):gsub('\\"', '"'):gsub('\\\\', '\\')
|
||||||
|
content = content:gsub("%s+", " "):gsub("^%s+", ""):gsub("%s+$", "")
|
||||||
|
|
||||||
|
return jarvis.cmd.ok(content)
|
||||||
184
resources/commands/outlook/_outlook.ps1
Normal file
184
resources/commands/outlook/_outlook.ps1
Normal file
|
|
@ -0,0 +1,184 @@
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory=$true)]
|
||||||
|
[ValidateSet("unread_count","latest","send_clipboard","summarize_inbox")]
|
||||||
|
[string]$Action,
|
||||||
|
|
||||||
|
[string]$Recipient = "",
|
||||||
|
[string]$Body = "",
|
||||||
|
[int]$Limit = 5
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
$OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
|
||||||
|
# Output protocol (parsed by Lua scripts):
|
||||||
|
# First line: "OK" or "ERR:<code>" or "ERR:<code>:<message>"
|
||||||
|
# Subsequent lines depend on Action:
|
||||||
|
# unread_count -> "<count>"
|
||||||
|
# latest -> "FROM\t<name>" / "SUBJECT\t<subj>" / "PREVIEW\t<body200>"
|
||||||
|
# send_clipboard -> "TO\t<name>" / "ADDR\t<email>" / "SUBJECT\t<subj>"
|
||||||
|
# summarize_inbox -> repeated "FROM\t<name>\tSUBJECT\t<subj>"
|
||||||
|
#
|
||||||
|
# All multiline strings get newlines collapsed to spaces before printing.
|
||||||
|
|
||||||
|
function Clean-Line([string]$s) {
|
||||||
|
if (-not $s) { return "" }
|
||||||
|
$s = $s -replace "`r", " " -replace "`n", " " -replace "`t", " "
|
||||||
|
return ($s -replace '\s+', ' ').Trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
function Fail($code, $msg = "") {
|
||||||
|
if ($msg) {
|
||||||
|
Write-Output ("ERR:{0}:{1}" -f $code, (Clean-Line $msg))
|
||||||
|
} else {
|
||||||
|
Write-Output ("ERR:{0}" -f $code)
|
||||||
|
}
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-OutlookSession {
|
||||||
|
try {
|
||||||
|
$ol = New-Object -ComObject Outlook.Application
|
||||||
|
$ns = $ol.GetNamespace("MAPI")
|
||||||
|
return @{ App = $ol; Ns = $ns }
|
||||||
|
} catch {
|
||||||
|
Fail "outlook_unavailable" $_.Exception.Message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Strip-Html([string]$s) {
|
||||||
|
if (-not $s) { return "" }
|
||||||
|
$s = [regex]::Replace($s, '<[^>]+>', ' ')
|
||||||
|
$s = $s -replace ' ', ' ' -replace '&', '&' -replace '<', '<' -replace '>', '>' -replace '"', '"' -replace "'", "'"
|
||||||
|
return Clean-Line $s
|
||||||
|
}
|
||||||
|
|
||||||
|
function Resolve-Recipient([object]$ns, [string]$query) {
|
||||||
|
if ([string]::IsNullOrWhiteSpace($query)) { return $null }
|
||||||
|
try {
|
||||||
|
$r = $ns.CreateRecipient($query)
|
||||||
|
$null = $r.Resolve()
|
||||||
|
if ($r.Resolved) {
|
||||||
|
$addr = $null
|
||||||
|
try { $addr = $r.AddressEntry.GetExchangeUser().PrimarySmtpAddress } catch { }
|
||||||
|
if (-not $addr) {
|
||||||
|
try { $addr = $r.AddressEntry.Address } catch { }
|
||||||
|
}
|
||||||
|
if (-not $addr) { $addr = $r.Name }
|
||||||
|
return @{ Name = $r.Name; Address = $addr }
|
||||||
|
}
|
||||||
|
} catch { }
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ($Action) {
|
||||||
|
"unread_count" {
|
||||||
|
$s = Get-OutlookSession
|
||||||
|
try {
|
||||||
|
$inbox = $s.Ns.GetDefaultFolder(6)
|
||||||
|
$count = 0
|
||||||
|
try { $count = [int]$inbox.UnReadItemCount } catch { }
|
||||||
|
if (-not $count) {
|
||||||
|
$count = ($inbox.Items | Where-Object { $_.UnRead -eq $true }).Count
|
||||||
|
}
|
||||||
|
Write-Output "OK"
|
||||||
|
Write-Output ([int]$count).ToString()
|
||||||
|
} catch {
|
||||||
|
Fail "inbox_failed" $_.Exception.Message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"latest" {
|
||||||
|
$s = Get-OutlookSession
|
||||||
|
try {
|
||||||
|
$inbox = $s.Ns.GetDefaultFolder(6)
|
||||||
|
$items = $inbox.Items
|
||||||
|
$items.Sort("[ReceivedTime]", $true)
|
||||||
|
$latest = $null
|
||||||
|
foreach ($it in $items) {
|
||||||
|
if ($it.Class -eq 43) { $latest = $it; break } # olMail = 43
|
||||||
|
}
|
||||||
|
if (-not $latest) { Fail "empty_inbox" }
|
||||||
|
$fromName = ""
|
||||||
|
try { $fromName = $latest.SenderName } catch { }
|
||||||
|
if (-not $fromName) { try { $fromName = $latest.SenderEmailAddress } catch { } }
|
||||||
|
$subject = ""
|
||||||
|
try { $subject = $latest.Subject } catch { }
|
||||||
|
$body = ""
|
||||||
|
try { $body = $latest.Body } catch { }
|
||||||
|
if (-not $body) {
|
||||||
|
try { $body = Strip-Html $latest.HTMLBody } catch { }
|
||||||
|
} else {
|
||||||
|
$body = Clean-Line $body
|
||||||
|
}
|
||||||
|
if ($body.Length -gt 200) { $body = $body.Substring(0, 200) + "..." }
|
||||||
|
Write-Output "OK"
|
||||||
|
Write-Output ("FROM`t{0}" -f (Clean-Line $fromName))
|
||||||
|
Write-Output ("SUBJECT`t{0}" -f (Clean-Line $subject))
|
||||||
|
Write-Output ("PREVIEW`t{0}" -f $body)
|
||||||
|
} catch {
|
||||||
|
Fail "latest_failed" $_.Exception.Message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"send_clipboard" {
|
||||||
|
if ([string]::IsNullOrWhiteSpace($Recipient)) { Fail "no_recipient" }
|
||||||
|
$s = Get-OutlookSession
|
||||||
|
try {
|
||||||
|
$resolved = Resolve-Recipient $s.Ns $Recipient
|
||||||
|
if (-not $resolved) { Fail "unresolved" $Recipient }
|
||||||
|
$bodyText = if ($Body) { $Body } else { "" }
|
||||||
|
$subject = ""
|
||||||
|
if ($bodyText) {
|
||||||
|
$firstLine = ($bodyText -split "`r?`n", 2)[0].Trim()
|
||||||
|
if ($firstLine.Length -gt 60) {
|
||||||
|
$subject = $firstLine.Substring(0, 60).Trim()
|
||||||
|
} else {
|
||||||
|
$subject = $firstLine
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ([string]::IsNullOrWhiteSpace($subject)) { $subject = "From J.A.R.V.I.S." }
|
||||||
|
|
||||||
|
$mail = $s.App.CreateItem(0)
|
||||||
|
$mail.To = $resolved.Address
|
||||||
|
$mail.Subject = $subject
|
||||||
|
$mail.Body = $bodyText
|
||||||
|
$mail.Send()
|
||||||
|
Write-Output "OK"
|
||||||
|
Write-Output ("TO`t{0}" -f (Clean-Line $resolved.Name))
|
||||||
|
Write-Output ("ADDR`t{0}" -f (Clean-Line $resolved.Address))
|
||||||
|
Write-Output ("SUBJECT`t{0}" -f (Clean-Line $subject))
|
||||||
|
} catch {
|
||||||
|
Fail "send_failed" $_.Exception.Message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"summarize_inbox" {
|
||||||
|
$s = Get-OutlookSession
|
||||||
|
try {
|
||||||
|
$inbox = $s.Ns.GetDefaultFolder(6)
|
||||||
|
$items = $inbox.Items
|
||||||
|
$items.Sort("[ReceivedTime]", $true)
|
||||||
|
$taken = 0
|
||||||
|
$lines = @()
|
||||||
|
foreach ($it in $items) {
|
||||||
|
if ($taken -ge $Limit) { break }
|
||||||
|
if ($it.Class -ne 43) { continue }
|
||||||
|
$unread = $false
|
||||||
|
try { $unread = [bool]$it.UnRead } catch { }
|
||||||
|
if (-not $unread) { continue }
|
||||||
|
$fromName = ""
|
||||||
|
try { $fromName = $it.SenderName } catch { }
|
||||||
|
$subj = ""
|
||||||
|
try { $subj = $it.Subject } catch { }
|
||||||
|
$lines += ("FROM`t{0}`tSUBJECT`t{1}" -f (Clean-Line $fromName), (Clean-Line $subj))
|
||||||
|
$taken += 1
|
||||||
|
}
|
||||||
|
Write-Output "OK"
|
||||||
|
foreach ($l in $lines) { Write-Output $l }
|
||||||
|
} catch {
|
||||||
|
Fail "summary_failed" $_.Exception.Message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
91
resources/commands/outlook/command.toml
Normal file
91
resources/commands/outlook/command.toml
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
[[commands]]
|
||||||
|
id = "outlook_unread_count"
|
||||||
|
type = "lua"
|
||||||
|
script = "unread_count.lua"
|
||||||
|
sandbox = "full"
|
||||||
|
timeout = 15000
|
||||||
|
|
||||||
|
[commands.phrases]
|
||||||
|
ru = [
|
||||||
|
"сколько у меня непрочитанных писем",
|
||||||
|
"сколько непрочитанных писем",
|
||||||
|
"проверь почту",
|
||||||
|
"проверь outlook",
|
||||||
|
"сколько новых писем",
|
||||||
|
]
|
||||||
|
en = [
|
||||||
|
"check unread mail",
|
||||||
|
"how many unread emails",
|
||||||
|
"check my inbox",
|
||||||
|
"unread email count",
|
||||||
|
"any new mail",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
[[commands]]
|
||||||
|
id = "outlook_latest"
|
||||||
|
type = "lua"
|
||||||
|
script = "latest.lua"
|
||||||
|
sandbox = "full"
|
||||||
|
timeout = 15000
|
||||||
|
|
||||||
|
[commands.phrases]
|
||||||
|
ru = [
|
||||||
|
"прочитай последнее письмо",
|
||||||
|
"что в последнем письме",
|
||||||
|
"последнее письмо",
|
||||||
|
"покажи последнее письмо",
|
||||||
|
]
|
||||||
|
en = [
|
||||||
|
"read last email",
|
||||||
|
"read latest email",
|
||||||
|
"what is the last email",
|
||||||
|
"show me the latest email",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
[[commands]]
|
||||||
|
id = "outlook_send_clipboard"
|
||||||
|
type = "lua"
|
||||||
|
script = "send_clipboard.lua"
|
||||||
|
sandbox = "full"
|
||||||
|
timeout = 20000
|
||||||
|
|
||||||
|
[commands.phrases]
|
||||||
|
ru = [
|
||||||
|
"отправь письмо {name}",
|
||||||
|
"отправь почту {name}",
|
||||||
|
"напиши письмо {name}",
|
||||||
|
"отправь это {name}",
|
||||||
|
]
|
||||||
|
en = [
|
||||||
|
"email this to {name}",
|
||||||
|
"send email to {name}",
|
||||||
|
"send this email to {name}",
|
||||||
|
"mail this to {name}",
|
||||||
|
]
|
||||||
|
|
||||||
|
[commands.slots.name]
|
||||||
|
entity = "person name"
|
||||||
|
|
||||||
|
|
||||||
|
[[commands]]
|
||||||
|
id = "outlook_summarize_inbox"
|
||||||
|
type = "lua"
|
||||||
|
script = "summarize_inbox.lua"
|
||||||
|
sandbox = "full"
|
||||||
|
timeout = 30000
|
||||||
|
|
||||||
|
[commands.phrases]
|
||||||
|
ru = [
|
||||||
|
"коротко расскажи про инбокс",
|
||||||
|
"расскажи что в инбоксе",
|
||||||
|
"сделай саммари инбокса",
|
||||||
|
"что нового в почте",
|
||||||
|
]
|
||||||
|
en = [
|
||||||
|
"summarize inbox",
|
||||||
|
"summarise my inbox",
|
||||||
|
"tell me about my inbox",
|
||||||
|
"what is new in my mail",
|
||||||
|
]
|
||||||
68
resources/commands/outlook/latest.lua
Normal file
68
resources/commands/outlook/latest.lua
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
-- Read the most recent email in the default Inbox: speak FROM + SUBJECT +
|
||||||
|
-- a 1-sentence preview (~200 chars, HTML stripped) of the body.
|
||||||
|
|
||||||
|
local lang = jarvis.context.language
|
||||||
|
local helper = jarvis.context.command_path .. "\\_outlook.ps1"
|
||||||
|
local cmd = string.format(
|
||||||
|
'powershell -NoProfile -ExecutionPolicy Bypass -File "%s" -Action latest',
|
||||||
|
helper
|
||||||
|
)
|
||||||
|
|
||||||
|
local res = jarvis.system.exec(cmd)
|
||||||
|
local out = (res.stdout or ""):gsub("\r", "")
|
||||||
|
|
||||||
|
if not res.success or out == "" then
|
||||||
|
jarvis.log("error", "outlook latest exec failed: " .. tostring(res.stderr))
|
||||||
|
return jarvis.cmd.error(lang == "ru"
|
||||||
|
and "Outlook недоступен — запусти Outlook сначала."
|
||||||
|
or "Outlook is unavailable — start Outlook first.")
|
||||||
|
end
|
||||||
|
|
||||||
|
local lines = {}
|
||||||
|
for line in out:gmatch("[^\n]+") do
|
||||||
|
table.insert(lines, line)
|
||||||
|
end
|
||||||
|
|
||||||
|
local status = lines[1] or ""
|
||||||
|
if status:sub(1, 3) == "ERR" then
|
||||||
|
if status:find("empty_inbox", 1, true) then
|
||||||
|
return jarvis.cmd.not_found(lang == "ru"
|
||||||
|
and "Входящие пусты."
|
||||||
|
or "Inbox is empty.")
|
||||||
|
end
|
||||||
|
jarvis.log("warn", "outlook latest: " .. status)
|
||||||
|
return jarvis.cmd.error(lang == "ru"
|
||||||
|
and "Outlook недоступен — запусти Outlook сначала."
|
||||||
|
or "Outlook is unavailable — start Outlook first.")
|
||||||
|
end
|
||||||
|
|
||||||
|
local from, subject, preview = "", "", ""
|
||||||
|
for i = 2, #lines do
|
||||||
|
local key, val = lines[i]:match("^([A-Z]+)\t(.*)$")
|
||||||
|
if key == "FROM" then from = val or "" end
|
||||||
|
if key == "SUBJECT" then subject = val or "" end
|
||||||
|
if key == "PREVIEW" then preview = val or "" end
|
||||||
|
end
|
||||||
|
|
||||||
|
if from == "" and subject == "" then
|
||||||
|
return jarvis.cmd.error(lang == "ru" and "Не получилось прочитать письмо." or "Could not read the email.")
|
||||||
|
end
|
||||||
|
|
||||||
|
local speech
|
||||||
|
if lang == "ru" then
|
||||||
|
speech = string.format("Последнее письмо от %s. Тема: %s. %s",
|
||||||
|
from ~= "" and from or "неизвестного отправителя",
|
||||||
|
subject ~= "" and subject or "без темы",
|
||||||
|
preview)
|
||||||
|
else
|
||||||
|
speech = string.format("Latest email from %s. Subject: %s. %s",
|
||||||
|
from ~= "" and from or "unknown sender",
|
||||||
|
subject ~= "" and subject or "no subject",
|
||||||
|
preview)
|
||||||
|
end
|
||||||
|
|
||||||
|
jarvis.system.notify(
|
||||||
|
lang == "ru" and "Последнее письмо" or "Latest email",
|
||||||
|
string.format("%s\n%s\n%s", from, subject, preview)
|
||||||
|
)
|
||||||
|
return jarvis.cmd.ok(speech)
|
||||||
103
resources/commands/outlook/send_clipboard.lua
Normal file
103
resources/commands/outlook/send_clipboard.lua
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
-- Send clipboard contents as an email to {name} via Outlook COM. The recipient
|
||||||
|
-- is resolved against the Global Address List + Contacts (Outlook's Recipient
|
||||||
|
-- .Resolve API). Subject = first line of the body truncated to 60 chars, or
|
||||||
|
-- "From J.A.R.V.I.S." for an empty clipboard.
|
||||||
|
|
||||||
|
local lang = jarvis.context.language
|
||||||
|
local slots = jarvis.context.slots or {}
|
||||||
|
local recipient = slots.name or ""
|
||||||
|
recipient = tostring(recipient):gsub("^%s+", ""):gsub("%s+$", "")
|
||||||
|
|
||||||
|
if recipient == "" then
|
||||||
|
return jarvis.cmd.not_found(lang == "ru"
|
||||||
|
and "Кому отправить? Назови имя."
|
||||||
|
or "Whom to send to? Please name them.")
|
||||||
|
end
|
||||||
|
|
||||||
|
local body = jarvis.system.clipboard.get() or ""
|
||||||
|
body = body:gsub("^%s+", ""):gsub("%s+$", "")
|
||||||
|
|
||||||
|
-- Write the body to a temp file so we don't have to escape it through the
|
||||||
|
-- PowerShell command-line. The PS helper reads it back via -Body parameter
|
||||||
|
-- value (passed as an arg). For very large bodies (>32 KB) the cmd-line
|
||||||
|
-- approach would break; in that case we read the file from inside PS.
|
||||||
|
local temp = jarvis.system.env("TEMP") or jarvis.system.env("TMP") or "C:\\Windows\\Temp"
|
||||||
|
local ts = jarvis.context.time.year .. jarvis.context.time.month .. jarvis.context.time.day
|
||||||
|
.. "_" .. jarvis.context.time.hour .. jarvis.context.time.minute .. jarvis.context.time.second
|
||||||
|
local body_path = temp .. "\\jarvis_outlook_body_" .. ts .. ".txt"
|
||||||
|
jarvis.fs.write(body_path, body)
|
||||||
|
|
||||||
|
-- Pass body via the file: the helper reads $Body if set, else falls back to
|
||||||
|
-- reading the saved file. To keep the helper simple we just inline the body
|
||||||
|
-- through -Body only when small; otherwise we still pass via -Body but quote
|
||||||
|
-- the path and let PS read it. We use the latter: PS reads its arg literally.
|
||||||
|
-- We instead encode the body so passing through args is safe.
|
||||||
|
local helper = jarvis.context.command_path .. "\\_outlook.ps1"
|
||||||
|
|
||||||
|
-- Build a single-line PowerShell expression that reads the body from the
|
||||||
|
-- temp file and invokes the helper. This avoids any shell-escape issues
|
||||||
|
-- with quotes/newlines in the clipboard.
|
||||||
|
local body_path_ps = body_path:gsub("'", "''")
|
||||||
|
local recipient_ps = recipient:gsub("'", "''")
|
||||||
|
local helper_ps = helper:gsub("'", "''")
|
||||||
|
local ps_expr = string.format(
|
||||||
|
"$b = Get-Content -LiteralPath '%s' -Raw -Encoding UTF8; if (-not $b) { $b = '' } else { $b = $b.TrimEnd() }; & '%s' -Action send_clipboard -Recipient '%s' -Body $b",
|
||||||
|
body_path_ps, helper_ps, recipient_ps
|
||||||
|
)
|
||||||
|
local cmd = string.format(
|
||||||
|
'powershell -NoProfile -ExecutionPolicy Bypass -Command "%s"',
|
||||||
|
ps_expr:gsub('"', '\\"')
|
||||||
|
)
|
||||||
|
|
||||||
|
local res = jarvis.system.exec(cmd)
|
||||||
|
jarvis.fs.remove(body_path)
|
||||||
|
|
||||||
|
local out = (res.stdout or ""):gsub("\r", "")
|
||||||
|
|
||||||
|
if not res.success or out == "" then
|
||||||
|
jarvis.log("error", "outlook send_clipboard exec failed: " .. tostring(res.stderr))
|
||||||
|
return jarvis.cmd.error(lang == "ru"
|
||||||
|
and "Outlook недоступен — запусти Outlook сначала."
|
||||||
|
or "Outlook is unavailable — start Outlook first.")
|
||||||
|
end
|
||||||
|
|
||||||
|
local lines = {}
|
||||||
|
for line in out:gmatch("[^\n]+") do table.insert(lines, line) end
|
||||||
|
|
||||||
|
local status = lines[1] or ""
|
||||||
|
if status:sub(1, 3) == "ERR" then
|
||||||
|
if status:find("unresolved", 1, true) then
|
||||||
|
return jarvis.cmd.not_found(lang == "ru"
|
||||||
|
and (string.format("Не нашёл контакт «%s».", recipient))
|
||||||
|
or (string.format("Could not find contact '%s'.", recipient)))
|
||||||
|
elseif status:find("no_recipient", 1, true) then
|
||||||
|
return jarvis.cmd.not_found(lang == "ru"
|
||||||
|
and "Кому отправить? Назови имя."
|
||||||
|
or "Whom to send to? Please name them.")
|
||||||
|
end
|
||||||
|
jarvis.log("warn", "outlook send_clipboard: " .. status)
|
||||||
|
return jarvis.cmd.error(lang == "ru"
|
||||||
|
and "Не получилось отправить — Outlook отказал."
|
||||||
|
or "Send failed — Outlook refused.")
|
||||||
|
end
|
||||||
|
|
||||||
|
local to_name, addr, subject = "", "", ""
|
||||||
|
for i = 2, #lines do
|
||||||
|
local key, val = lines[i]:match("^([A-Z]+)\t(.*)$")
|
||||||
|
if key == "TO" then to_name = val or "" end
|
||||||
|
if key == "ADDR" then addr = val or "" end
|
||||||
|
if key == "SUBJECT" then subject = val or "" end
|
||||||
|
end
|
||||||
|
|
||||||
|
local speech
|
||||||
|
if lang == "ru" then
|
||||||
|
speech = string.format("Отправил %s. Тема: %s.", to_name ~= "" and to_name or recipient, subject)
|
||||||
|
else
|
||||||
|
speech = string.format("Sent to %s. Subject: %s.", to_name ~= "" and to_name or recipient, subject)
|
||||||
|
end
|
||||||
|
|
||||||
|
jarvis.system.notify(
|
||||||
|
lang == "ru" and "Письмо отправлено" or "Email sent",
|
||||||
|
string.format("%s <%s>\n%s", to_name, addr, subject)
|
||||||
|
)
|
||||||
|
return jarvis.cmd.ok(speech)
|
||||||
79
resources/commands/outlook/summarize_inbox.lua
Normal file
79
resources/commands/outlook/summarize_inbox.lua
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
-- Summarize the last few unread emails into one paragraph via the LLM.
|
||||||
|
-- Pulls FROM+SUBJECT of up to 5 unread items, then asks the model for a
|
||||||
|
-- short paragraph in the user's language.
|
||||||
|
|
||||||
|
local lang = jarvis.context.language
|
||||||
|
local helper = jarvis.context.command_path .. "\\_outlook.ps1"
|
||||||
|
local cmd = string.format(
|
||||||
|
'powershell -NoProfile -ExecutionPolicy Bypass -File "%s" -Action summarize_inbox -Limit 5',
|
||||||
|
helper
|
||||||
|
)
|
||||||
|
|
||||||
|
local res = jarvis.system.exec(cmd)
|
||||||
|
local out = (res.stdout or ""):gsub("\r", "")
|
||||||
|
|
||||||
|
if not res.success or out == "" then
|
||||||
|
jarvis.log("error", "outlook summarize_inbox exec failed: " .. tostring(res.stderr))
|
||||||
|
return jarvis.cmd.error(lang == "ru"
|
||||||
|
and "Outlook недоступен — запусти Outlook сначала."
|
||||||
|
or "Outlook is unavailable — start Outlook first.")
|
||||||
|
end
|
||||||
|
|
||||||
|
local lines = {}
|
||||||
|
for line in out:gmatch("[^\n]+") do table.insert(lines, line) end
|
||||||
|
|
||||||
|
local status = lines[1] or ""
|
||||||
|
if status:sub(1, 3) == "ERR" then
|
||||||
|
jarvis.log("warn", "outlook summarize_inbox: " .. status)
|
||||||
|
return jarvis.cmd.error(lang == "ru"
|
||||||
|
and "Outlook недоступен — запусти Outlook сначала."
|
||||||
|
or "Outlook is unavailable — start Outlook first.")
|
||||||
|
end
|
||||||
|
|
||||||
|
local emails = {}
|
||||||
|
for i = 2, #lines do
|
||||||
|
local from, subject = lines[i]:match("^FROM\t(.-)\tSUBJECT\t(.*)$")
|
||||||
|
if from then
|
||||||
|
table.insert(emails, { from = from, subject = subject or "" })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if #emails == 0 then
|
||||||
|
return jarvis.cmd.not_found(lang == "ru"
|
||||||
|
and "Непрочитанных писем нет."
|
||||||
|
or "No unread emails.")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Build the prompt
|
||||||
|
local list_str = ""
|
||||||
|
for i, m in ipairs(emails) do
|
||||||
|
list_str = list_str .. string.format("%d. From: %s | Subject: %s\n", i, m.from, m.subject)
|
||||||
|
end
|
||||||
|
|
||||||
|
local sys_prompt = lang == "ru"
|
||||||
|
and "Ты помощник по почте. Кратко опиши, что лежит в инбоксе, ОДНИМ абзацем (2-4 предложения). Сгруппируй по темам/отправителям если возможно. Без преамбулы, без перечисления списком."
|
||||||
|
or "You are an email assistant. Briefly describe the inbox in ONE paragraph (2-4 sentences). Group by topic/sender when possible. No preamble, no bullet list."
|
||||||
|
|
||||||
|
local user_prompt = (lang == "ru" and "Непрочитанные письма:\n" or "Unread emails:\n") .. list_str
|
||||||
|
|
||||||
|
local reply = jarvis.llm(
|
||||||
|
{ { role = "system", content = sys_prompt },
|
||||||
|
{ role = "user", content = user_prompt } },
|
||||||
|
{ max_tokens = 250, temperature = 0.4 }
|
||||||
|
)
|
||||||
|
|
||||||
|
if not reply or reply == "" then
|
||||||
|
-- Fallback: simple flat enumeration when LLM is unavailable.
|
||||||
|
local parts = {}
|
||||||
|
for _, m in ipairs(emails) do
|
||||||
|
table.insert(parts, string.format("%s — %s", m.from, m.subject))
|
||||||
|
end
|
||||||
|
local fallback = (lang == "ru"
|
||||||
|
and string.format("Непрочитанных: %d. ", #emails)
|
||||||
|
or string.format("Unread: %d. ", #emails)) .. table.concat(parts, "; ") .. "."
|
||||||
|
jarvis.system.notify(lang == "ru" and "Инбокс" or "Inbox", fallback)
|
||||||
|
return jarvis.cmd.ok(fallback)
|
||||||
|
end
|
||||||
|
|
||||||
|
jarvis.system.notify(lang == "ru" and "Инбокс" or "Inbox", reply)
|
||||||
|
return jarvis.cmd.ok(reply)
|
||||||
55
resources/commands/outlook/unread_count.lua
Normal file
55
resources/commands/outlook/unread_count.lua
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
-- Voice-controlled Outlook: report number of unread emails in default Inbox.
|
||||||
|
-- The PowerShell helper talks to Outlook via COM; if Outlook is closed or
|
||||||
|
-- COM fails we degrade gracefully with an explanatory message.
|
||||||
|
|
||||||
|
local lang = jarvis.context.language
|
||||||
|
local helper = jarvis.context.command_path .. "\\_outlook.ps1"
|
||||||
|
local cmd = string.format(
|
||||||
|
'powershell -NoProfile -ExecutionPolicy Bypass -File "%s" -Action unread_count',
|
||||||
|
helper
|
||||||
|
)
|
||||||
|
|
||||||
|
local res = jarvis.system.exec(cmd)
|
||||||
|
local out = (res.stdout or ""):gsub("\r", "")
|
||||||
|
|
||||||
|
if not res.success or out == "" then
|
||||||
|
jarvis.log("error", "outlook unread_count exec failed: " .. tostring(res.stderr))
|
||||||
|
return jarvis.cmd.error(lang == "ru"
|
||||||
|
and "Outlook недоступен — запусти Outlook сначала."
|
||||||
|
or "Outlook is unavailable — start Outlook first.")
|
||||||
|
end
|
||||||
|
|
||||||
|
local first_line, rest = out:match("^([^\n]*)\n?(.*)$")
|
||||||
|
first_line = first_line or ""
|
||||||
|
|
||||||
|
if first_line:sub(1, 3) == "ERR" then
|
||||||
|
jarvis.log("warn", "outlook unread_count: " .. first_line)
|
||||||
|
return jarvis.cmd.error(lang == "ru"
|
||||||
|
and "Outlook недоступен — запусти Outlook сначала."
|
||||||
|
or "Outlook is unavailable — start Outlook first.")
|
||||||
|
end
|
||||||
|
|
||||||
|
local count_str = (rest or ""):match("(%d+)") or "0"
|
||||||
|
local count = tonumber(count_str) or 0
|
||||||
|
|
||||||
|
local speech
|
||||||
|
if lang == "ru" then
|
||||||
|
if count == 0 then
|
||||||
|
speech = "Непрочитанных писем нет."
|
||||||
|
elseif count == 1 then
|
||||||
|
speech = "Одно непрочитанное письмо."
|
||||||
|
else
|
||||||
|
speech = string.format("Непрочитанных писем: %d.", count)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
if count == 0 then
|
||||||
|
speech = "No unread emails."
|
||||||
|
elseif count == 1 then
|
||||||
|
speech = "One unread email."
|
||||||
|
else
|
||||||
|
speech = string.format("You have %d unread emails.", count)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
jarvis.system.notify(lang == "ru" and "Outlook" or "Outlook", speech)
|
||||||
|
return jarvis.cmd.ok(speech)
|
||||||
|
|
@ -1,3 +1,10 @@
|
||||||
|
-- Reminder — schedule a one-shot Speak action via the persistent scheduler.
|
||||||
|
--
|
||||||
|
-- Why this rewrite: the previous version spawned `Start-Sleep` PowerShell
|
||||||
|
-- subprocesses that didn't survive jarvis-app restart. Now we use
|
||||||
|
-- `jarvis.scheduler.add(...)` which persists to schedule.json and fires
|
||||||
|
-- correctly even if the daemon is restarted in between.
|
||||||
|
|
||||||
local lang = jarvis.context.language
|
local lang = jarvis.context.language
|
||||||
local phrase = (jarvis.context.phrase or ""):lower()
|
local phrase = (jarvis.context.phrase or ""):lower()
|
||||||
|
|
||||||
|
|
@ -5,9 +12,8 @@ local triggers = {
|
||||||
"поставь напоминание через", "установи таймер на",
|
"поставь напоминание через", "установи таймер на",
|
||||||
"напомни мне через", "напомни через", "напомни",
|
"напомни мне через", "напомни через", "напомни",
|
||||||
"remind me in", "set reminder in", "set timer for", "timer for",
|
"remind me in", "set reminder in", "set timer for", "timer for",
|
||||||
"постав нагадування через", "нагадай через", "таймер на",
|
"таймер на",
|
||||||
}
|
}
|
||||||
|
|
||||||
local rest = phrase
|
local rest = phrase
|
||||||
for _, t in ipairs(triggers) do
|
for _, t in ipairs(triggers) do
|
||||||
local s, e = string.find(rest, t, 1, true)
|
local s, e = string.find(rest, t, 1, true)
|
||||||
|
|
@ -19,19 +25,20 @@ end
|
||||||
rest = rest:gsub("^%s+", "")
|
rest = rest:gsub("^%s+", "")
|
||||||
|
|
||||||
local unit_words = {
|
local unit_words = {
|
||||||
["секунд"] = 1, ["секунду"] = 1, ["секунды"] = 1,
|
["секунд"] = 1, ["секунду"] = 1, ["секунды"] = 1,
|
||||||
["second"] = 1, ["seconds"] = 1, ["sec"] = 1,
|
["second"] = 1, ["seconds"] = 1, ["sec"] = 1,
|
||||||
["минут"] = 60, ["минуту"] = 60, ["минуты"] = 60, ["мин"] = 60,
|
["минут"] = 60, ["минуту"] = 60, ["минуты"] = 60, ["мин"] = 60,
|
||||||
["minute"] = 60, ["minutes"] = 60, ["min"] = 60,
|
["minute"] = 60, ["minutes"] = 60, ["min"] = 60,
|
||||||
["час"] = 3600, ["часа"] = 3600, ["часов"] = 3600, ["часик"] = 3600,
|
["час"] = 3600, ["часа"] = 3600, ["часов"] = 3600, ["часик"] = 3600,
|
||||||
["hour"] = 3600, ["hours"] = 3600, ["hr"] = 3600,
|
["hour"] = 3600, ["hours"] = 3600, ["hr"] = 3600,
|
||||||
}
|
}
|
||||||
local rus_numerals = {
|
local rus_numerals = {
|
||||||
["один"] = 1, ["одну"] = 1, ["одна"] = 1, ["полторы"] = 1, ["полтора"] = 1,
|
["один"] = 1, ["одну"] = 1, ["одна"] = 1,
|
||||||
|
["полторы"] = 1, ["полтора"] = 1,
|
||||||
["два"] = 2, ["две"] = 2, ["три"] = 3, ["четыре"] = 4, ["пять"] = 5,
|
["два"] = 2, ["две"] = 2, ["три"] = 3, ["четыре"] = 4, ["пять"] = 5,
|
||||||
["шесть"] = 6, ["семь"] = 7, ["восемь"] = 8, ["девять"] = 9, ["десять"] = 10,
|
["шесть"] = 6, ["семь"] = 7, ["восемь"] = 8, ["девять"] = 9, ["десять"] = 10,
|
||||||
["пятнадцать"] = 15, ["двадцать"] = 20, ["тридцать"] = 30, ["сорок"] = 40,
|
["пятнадцать"] = 15, ["двадцать"] = 20, ["тридцать"] = 30, ["сорок"] = 40,
|
||||||
["пятьдесят"] = 50, ["час"] = 60, -- "через час" without number
|
["пятьдесят"] = 50,
|
||||||
}
|
}
|
||||||
|
|
||||||
local number, unit, body = nil, nil, nil
|
local number, unit, body = nil, nil, nil
|
||||||
|
|
@ -85,35 +92,38 @@ local function human_time(sec)
|
||||||
return string.format("%d ч %d мин", math.floor(sec/3600), math.floor((sec%3600)/60))
|
return string.format("%d ч %d мин", math.floor(sec/3600), math.floor((sec%3600)/60))
|
||||||
end
|
end
|
||||||
|
|
||||||
local title_esc = (lang == "ru" and "Напоминание J.A.R.V.I.S." or "J.A.R.V.I.S. reminder"):gsub("'", "''")
|
-- Build the scheduler entry. Scheduler's "in N minutes" wants integer minutes
|
||||||
local body_esc = body:gsub("'", "''")
|
-- so we round up for sub-minute timers; users asking for "10 seconds" will get
|
||||||
local sapi_text = ((lang == "ru" and "Сэр, напоминаю: " or "Reminder: ") .. body):gsub("'", "''"):gsub("\r?\n", " ")
|
-- ~1 minute. Honest trade-off vs the more complex previous detached-PS approach.
|
||||||
|
local minutes = math.max(1, math.floor((total_seconds + 59) / 60))
|
||||||
|
local schedule_str = "in " .. tostring(minutes) .. " minutes"
|
||||||
|
|
||||||
local ps = string.format(
|
local speak_text = (lang == "ru" and "Сэр, напоминаю: " or "Reminder: ") .. body
|
||||||
[[Start-Sleep -Seconds %d; ]]
|
|
||||||
.. [[$null = New-Object -ComObject Shell.Application; ]]
|
|
||||||
.. [[try { ]]
|
|
||||||
.. [[ Import-Module BurntToast -ErrorAction SilentlyContinue; ]]
|
|
||||||
.. [[ New-BurntToastNotification -Text '%s', '%s' -ErrorAction SilentlyContinue ]]
|
|
||||||
.. [[} catch {}; ]]
|
|
||||||
.. [[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'); ]]
|
|
||||||
.. [[$wsh = New-Object -ComObject WScript.Shell; $wsh.Popup('%s', 30, '%s', 64) | Out-Null]],
|
|
||||||
total_seconds, title_esc, body_esc, sapi_text, body_esc, title_esc
|
|
||||||
)
|
|
||||||
local cmd = string.format(
|
|
||||||
'powershell -NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -Command "%s"',
|
|
||||||
ps:gsub('"', '\\"')
|
|
||||||
)
|
|
||||||
|
|
||||||
jarvis.system.exec("start /b " .. cmd)
|
local ok, err = pcall(function()
|
||||||
|
jarvis.scheduler.add({
|
||||||
|
name = lang == "ru" and ("Таймер: " .. body) or ("Timer: " .. body),
|
||||||
|
schedule = schedule_str,
|
||||||
|
action = { type = "speak", text = speak_text },
|
||||||
|
})
|
||||||
|
end)
|
||||||
|
|
||||||
|
if not ok then
|
||||||
|
jarvis.log("error", "reminder: scheduler.add failed: " .. tostring(err))
|
||||||
|
return jarvis.cmd.error(lang == "ru"
|
||||||
|
and "Не получилось поставить напоминание."
|
||||||
|
or "Couldn't set reminder.")
|
||||||
|
end
|
||||||
|
|
||||||
jarvis.system.notify(
|
jarvis.system.notify(
|
||||||
lang == "ru" and "Таймер установлен" or "Timer set",
|
lang == "ru" and "Таймер установлен" or "Timer set",
|
||||||
string.format(lang == "ru" and "Через %s: %s" or "In %s: %s", human_time(total_seconds), body)
|
string.format(lang == "ru" and "Через %s: %s" or "In %s: %s",
|
||||||
|
human_time(total_seconds), body)
|
||||||
)
|
)
|
||||||
jarvis.log("info", string.format("reminder in %ds: %s", total_seconds, body))
|
jarvis.log("info", string.format("reminder in %ds (rounded to %d min): %s",
|
||||||
jarvis.audio.play_ok()
|
total_seconds, minutes, body))
|
||||||
return { chain = false }
|
|
||||||
|
return jarvis.cmd.ok(string.format(
|
||||||
|
lang == "ru" and "Хорошо, напомню через %s." or "Will remind you in %s.",
|
||||||
|
human_time(total_seconds)
|
||||||
|
))
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,13 @@ local helper = jarvis.context.command_path .. "\\_sysinfo.ps1"
|
||||||
local cmd = string.format('powershell -NoProfile -ExecutionPolicy Bypass -File "%s" -Topic all', helper)
|
local cmd = string.format('powershell -NoProfile -ExecutionPolicy Bypass -File "%s" -Topic all', helper)
|
||||||
local res = jarvis.system.exec(cmd)
|
local res = jarvis.system.exec(cmd)
|
||||||
local text = (res.stdout or ""):gsub("[\r\n]+$", "")
|
local text = (res.stdout or ""):gsub("[\r\n]+$", "")
|
||||||
if res.success and text ~= "" then
|
if not res.success or text == "" then
|
||||||
jarvis.system.notify("Статус системы", text)
|
|
||||||
jarvis.log("info", text)
|
|
||||||
jarvis.audio.play_ok()
|
|
||||||
else
|
|
||||||
jarvis.log("error", "sysinfo all failed: " .. tostring(res.stderr))
|
jarvis.log("error", "sysinfo all failed: " .. tostring(res.stderr))
|
||||||
jarvis.audio.play_error()
|
return jarvis.cmd.error("Не получилось собрать состояние системы.")
|
||||||
end
|
end
|
||||||
return { chain = false }
|
jarvis.system.notify("Статус системы", text)
|
||||||
|
jarvis.log("info", text)
|
||||||
|
-- Multi-line output reads more naturally as a single sentence with commas
|
||||||
|
-- between metrics, so TTS doesn't over-pause on linebreaks.
|
||||||
|
local spoken = text:gsub("[\r\n]+", ", ")
|
||||||
|
return jarvis.cmd.ok(spoken)
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,10 @@ local helper = jarvis.context.command_path .. "\\_sysinfo.ps1"
|
||||||
local cmd = string.format('powershell -NoProfile -ExecutionPolicy Bypass -File "%s" -Topic battery', helper)
|
local cmd = string.format('powershell -NoProfile -ExecutionPolicy Bypass -File "%s" -Topic battery', helper)
|
||||||
local res = jarvis.system.exec(cmd)
|
local res = jarvis.system.exec(cmd)
|
||||||
local text = (res.stdout or ""):gsub("[\r\n]+$", "")
|
local text = (res.stdout or ""):gsub("[\r\n]+$", "")
|
||||||
if res.success and text ~= "" then
|
if not res.success or text == "" then
|
||||||
jarvis.system.notify("Батарея", text)
|
|
||||||
jarvis.log("info", text)
|
|
||||||
jarvis.audio.play_ok()
|
|
||||||
else
|
|
||||||
jarvis.log("error", "battery info failed: " .. tostring(res.stderr))
|
jarvis.log("error", "battery info failed: " .. tostring(res.stderr))
|
||||||
jarvis.audio.play_error()
|
return jarvis.cmd.error("Не получилось узнать заряд.")
|
||||||
end
|
end
|
||||||
return { chain = false }
|
jarvis.system.notify("Батарея", text)
|
||||||
|
jarvis.log("info", text)
|
||||||
|
return jarvis.cmd.ok(text)
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,10 @@ local helper = jarvis.context.command_path .. "\\_sysinfo.ps1"
|
||||||
local cmd = string.format('powershell -NoProfile -ExecutionPolicy Bypass -File "%s" -Topic cpu', helper)
|
local cmd = string.format('powershell -NoProfile -ExecutionPolicy Bypass -File "%s" -Topic cpu', helper)
|
||||||
local res = jarvis.system.exec(cmd)
|
local res = jarvis.system.exec(cmd)
|
||||||
local text = (res.stdout or ""):gsub("[\r\n]+$", "")
|
local text = (res.stdout or ""):gsub("[\r\n]+$", "")
|
||||||
if res.success and text ~= "" then
|
if not res.success or text == "" then
|
||||||
jarvis.system.notify("CPU", text)
|
|
||||||
jarvis.log("info", text)
|
|
||||||
jarvis.audio.play_ok()
|
|
||||||
else
|
|
||||||
jarvis.log("error", "cpu info failed: " .. tostring(res.stderr))
|
jarvis.log("error", "cpu info failed: " .. tostring(res.stderr))
|
||||||
jarvis.audio.play_error()
|
return jarvis.cmd.error("Не получилось узнать загрузку процессора.")
|
||||||
end
|
end
|
||||||
return { chain = false }
|
jarvis.system.notify("CPU", text)
|
||||||
|
jarvis.log("info", text)
|
||||||
|
return jarvis.cmd.ok(text)
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,10 @@ local helper = jarvis.context.command_path .. "\\_sysinfo.ps1"
|
||||||
local cmd = string.format('powershell -NoProfile -ExecutionPolicy Bypass -File "%s" -Topic disk', helper)
|
local cmd = string.format('powershell -NoProfile -ExecutionPolicy Bypass -File "%s" -Topic disk', helper)
|
||||||
local res = jarvis.system.exec(cmd)
|
local res = jarvis.system.exec(cmd)
|
||||||
local text = (res.stdout or ""):gsub("[\r\n]+$", "")
|
local text = (res.stdout or ""):gsub("[\r\n]+$", "")
|
||||||
if res.success and text ~= "" then
|
if not res.success or text == "" then
|
||||||
jarvis.system.notify("Диск", text)
|
|
||||||
jarvis.log("info", text)
|
|
||||||
jarvis.audio.play_ok()
|
|
||||||
else
|
|
||||||
jarvis.log("error", "disk info failed: " .. tostring(res.stderr))
|
jarvis.log("error", "disk info failed: " .. tostring(res.stderr))
|
||||||
jarvis.audio.play_error()
|
return jarvis.cmd.error("Не получилось узнать диск.")
|
||||||
end
|
end
|
||||||
return { chain = false }
|
jarvis.system.notify("Диск", text)
|
||||||
|
jarvis.log("info", text)
|
||||||
|
return jarvis.cmd.ok(text)
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,10 @@ local helper = jarvis.context.command_path .. "\\_sysinfo.ps1"
|
||||||
local cmd = string.format('powershell -NoProfile -ExecutionPolicy Bypass -File "%s" -Topic ram', helper)
|
local cmd = string.format('powershell -NoProfile -ExecutionPolicy Bypass -File "%s" -Topic ram', helper)
|
||||||
local res = jarvis.system.exec(cmd)
|
local res = jarvis.system.exec(cmd)
|
||||||
local text = (res.stdout or ""):gsub("[\r\n]+$", "")
|
local text = (res.stdout or ""):gsub("[\r\n]+$", "")
|
||||||
if res.success and text ~= "" then
|
if not res.success or text == "" then
|
||||||
jarvis.system.notify("Память", text)
|
|
||||||
jarvis.log("info", text)
|
|
||||||
jarvis.audio.play_ok()
|
|
||||||
else
|
|
||||||
jarvis.log("error", "ram info failed: " .. tostring(res.stderr))
|
jarvis.log("error", "ram info failed: " .. tostring(res.stderr))
|
||||||
jarvis.audio.play_error()
|
return jarvis.cmd.error("Не получилось узнать память.")
|
||||||
end
|
end
|
||||||
return { chain = false }
|
jarvis.system.notify("Память", text)
|
||||||
|
jarvis.log("info", text)
|
||||||
|
return jarvis.cmd.ok(text)
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,10 @@ local helper = jarvis.context.command_path .. "\\_sysinfo.ps1"
|
||||||
local cmd = string.format('powershell -NoProfile -ExecutionPolicy Bypass -File "%s" -Topic time', helper)
|
local cmd = string.format('powershell -NoProfile -ExecutionPolicy Bypass -File "%s" -Topic time', helper)
|
||||||
local res = jarvis.system.exec(cmd)
|
local res = jarvis.system.exec(cmd)
|
||||||
local text = (res.stdout or ""):gsub("[\r\n]+$", "")
|
local text = (res.stdout or ""):gsub("[\r\n]+$", "")
|
||||||
if res.success and text ~= "" then
|
if not res.success or text == "" then
|
||||||
jarvis.system.notify("Время", text)
|
|
||||||
jarvis.log("info", text)
|
|
||||||
jarvis.audio.play_ok()
|
|
||||||
else
|
|
||||||
jarvis.log("error", "time info failed: " .. tostring(res.stderr))
|
jarvis.log("error", "time info failed: " .. tostring(res.stderr))
|
||||||
jarvis.audio.play_error()
|
return jarvis.cmd.error("Не получилось узнать время.")
|
||||||
end
|
end
|
||||||
return { chain = false }
|
jarvis.system.notify("Время", text)
|
||||||
|
jarvis.log("info", text)
|
||||||
|
return jarvis.cmd.ok(text)
|
||||||
|
|
|
||||||
121
resources/commands/time_tracker/command.toml
Normal file
121
resources/commands/time_tracker/command.toml
Normal file
|
|
@ -0,0 +1,121 @@
|
||||||
|
# Voice-driven daily time tracking.
|
||||||
|
#
|
||||||
|
# Storage: jarvis.state on this pack — a single key "data" holding
|
||||||
|
# { current_session_start = <ts>|nil, sessions = { {start=ts,end=ts}, ... } }
|
||||||
|
#
|
||||||
|
# tracker.start marks the beginning of an active session.
|
||||||
|
# tracker.stop closes the open session and pushes it onto the array.
|
||||||
|
# tracker.today / tracker.week report totals (sum of completed +
|
||||||
|
# current open session if any).
|
||||||
|
# tracker.reset wipes today's entries; speech reply confirms what was wiped.
|
||||||
|
|
||||||
|
[[commands]]
|
||||||
|
id = "tracker.start"
|
||||||
|
type = "lua"
|
||||||
|
script = "start.lua"
|
||||||
|
sandbox = "standard"
|
||||||
|
timeout = 3000
|
||||||
|
|
||||||
|
[commands.phrases]
|
||||||
|
ru = [
|
||||||
|
"начни отсчёт",
|
||||||
|
"начни отсчет",
|
||||||
|
"запусти трекер",
|
||||||
|
"я начинаю работу",
|
||||||
|
"начало рабочего дня",
|
||||||
|
]
|
||||||
|
en = [
|
||||||
|
"start tracking",
|
||||||
|
"I'm starting work",
|
||||||
|
"begin tracking",
|
||||||
|
"start the tracker",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
[[commands]]
|
||||||
|
id = "tracker.stop"
|
||||||
|
type = "lua"
|
||||||
|
script = "stop.lua"
|
||||||
|
sandbox = "standard"
|
||||||
|
timeout = 3000
|
||||||
|
|
||||||
|
[commands.phrases]
|
||||||
|
ru = [
|
||||||
|
"закончи отсчёт",
|
||||||
|
"закончи отсчет",
|
||||||
|
"останови трекер",
|
||||||
|
"я закончил работу",
|
||||||
|
"конец рабочего дня",
|
||||||
|
]
|
||||||
|
en = [
|
||||||
|
"stop tracking",
|
||||||
|
"I'm done",
|
||||||
|
"end the tracker",
|
||||||
|
"stop the tracker",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
[[commands]]
|
||||||
|
id = "tracker.today"
|
||||||
|
type = "lua"
|
||||||
|
script = "today.lua"
|
||||||
|
sandbox = "standard"
|
||||||
|
timeout = 3000
|
||||||
|
|
||||||
|
[commands.phrases]
|
||||||
|
ru = [
|
||||||
|
"сколько я работаю сегодня",
|
||||||
|
"сколько отработал сегодня",
|
||||||
|
"сколько часов сегодня",
|
||||||
|
"сколько времени за сегодня",
|
||||||
|
]
|
||||||
|
en = [
|
||||||
|
"how long today",
|
||||||
|
"how much have I worked today",
|
||||||
|
"hours today",
|
||||||
|
"time today",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
[[commands]]
|
||||||
|
id = "tracker.week"
|
||||||
|
type = "lua"
|
||||||
|
script = "week.lua"
|
||||||
|
sandbox = "standard"
|
||||||
|
timeout = 3000
|
||||||
|
|
||||||
|
[commands.phrases]
|
||||||
|
ru = [
|
||||||
|
"сколько на этой неделе",
|
||||||
|
"сколько отработал на неделе",
|
||||||
|
"часы за неделю",
|
||||||
|
"статистика за неделю",
|
||||||
|
]
|
||||||
|
en = [
|
||||||
|
"hours this week",
|
||||||
|
"how long this week",
|
||||||
|
"time this week",
|
||||||
|
"weekly hours",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
[[commands]]
|
||||||
|
id = "tracker.reset"
|
||||||
|
type = "lua"
|
||||||
|
script = "reset.lua"
|
||||||
|
sandbox = "standard"
|
||||||
|
timeout = 3000
|
||||||
|
|
||||||
|
[commands.phrases]
|
||||||
|
ru = [
|
||||||
|
"сбрось трекер",
|
||||||
|
"обнули трекер",
|
||||||
|
"очисти трекер",
|
||||||
|
"сбрось отсчёт",
|
||||||
|
]
|
||||||
|
en = [
|
||||||
|
"reset tracker",
|
||||||
|
"clear tracker",
|
||||||
|
"wipe today's tracker",
|
||||||
|
"reset today's tracker",
|
||||||
|
]
|
||||||
59
resources/commands/time_tracker/reset.lua
Normal file
59
resources/commands/time_tracker/reset.lua
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
-- tracker.reset — wipe today's entries (and the open session).
|
||||||
|
local lang = jarvis.context.language or "ru"
|
||||||
|
local now = jarvis.context.time.timestamp or os.time()
|
||||||
|
|
||||||
|
local t = jarvis.context.time
|
||||||
|
local h = tonumber(t.hour) or 0
|
||||||
|
local mi = tonumber(t.minute) or 0
|
||||||
|
local s = tonumber(t.second) or 0
|
||||||
|
local today_start = now - (h * 3600 + mi * 60 + s)
|
||||||
|
|
||||||
|
local data = jarvis.state.get("data") or {}
|
||||||
|
local sessions = data.sessions or {}
|
||||||
|
|
||||||
|
local kept = {}
|
||||||
|
local dropped = 0
|
||||||
|
for _, sess in ipairs(sessions) do
|
||||||
|
local sstart = tonumber(sess.start) or 0
|
||||||
|
if sstart < today_start then
|
||||||
|
table.insert(kept, sess)
|
||||||
|
else
|
||||||
|
dropped = dropped + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local had_open = data.current_session_start ~= nil
|
||||||
|
data.sessions = kept
|
||||||
|
data.current_session_start = nil
|
||||||
|
jarvis.state.set("data", data)
|
||||||
|
|
||||||
|
if dropped == 0 and not had_open then
|
||||||
|
return jarvis.cmd.ok(lang == "ru"
|
||||||
|
and "Сегодня сбрасывать нечего, сэр."
|
||||||
|
or "Nothing to reset today, sir.")
|
||||||
|
end
|
||||||
|
|
||||||
|
local confirm
|
||||||
|
if lang == "ru" then
|
||||||
|
confirm = "Подтверждаю: трекер за сегодня очищен"
|
||||||
|
if dropped > 0 then
|
||||||
|
local sw
|
||||||
|
local n10 = dropped % 10; local n100 = dropped % 100
|
||||||
|
if n100 >= 11 and n100 <= 14 then sw = "сессий"
|
||||||
|
elseif n10 == 1 then sw = "сессия"
|
||||||
|
elseif n10 >= 2 and n10 <= 4 then sw = "сессии"
|
||||||
|
else sw = "сессий" end
|
||||||
|
confirm = confirm .. ", удалено " .. dropped .. " " .. sw
|
||||||
|
end
|
||||||
|
if had_open then confirm = confirm .. ", активный отсчёт остановлен" end
|
||||||
|
confirm = confirm .. "."
|
||||||
|
else
|
||||||
|
confirm = "Confirmed: tracker for today cleared"
|
||||||
|
if dropped > 0 then
|
||||||
|
confirm = confirm .. ", " .. dropped .. " session" .. (dropped == 1 and "" or "s") .. " removed"
|
||||||
|
end
|
||||||
|
if had_open then confirm = confirm .. ", running session stopped" end
|
||||||
|
confirm = confirm .. "."
|
||||||
|
end
|
||||||
|
|
||||||
|
return jarvis.cmd.ok(confirm)
|
||||||
22
resources/commands/time_tracker/start.lua
Normal file
22
resources/commands/time_tracker/start.lua
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
-- tracker.start — open a new active session.
|
||||||
|
local lang = jarvis.context.language or "ru"
|
||||||
|
local now = jarvis.context.time.timestamp or os.time()
|
||||||
|
|
||||||
|
local data = jarvis.state.get("data") or {}
|
||||||
|
local sessions = data.sessions or {}
|
||||||
|
|
||||||
|
if data.current_session_start then
|
||||||
|
-- already running — don't overwrite; just acknowledge.
|
||||||
|
return jarvis.cmd.ok(lang == "ru"
|
||||||
|
and "Уже считаю, сэр."
|
||||||
|
or "Already tracking, sir.")
|
||||||
|
end
|
||||||
|
|
||||||
|
data.current_session_start = now
|
||||||
|
data.sessions = sessions
|
||||||
|
|
||||||
|
jarvis.state.set("data", data)
|
||||||
|
|
||||||
|
return jarvis.cmd.ok(lang == "ru"
|
||||||
|
and "Отсчёт пошёл."
|
||||||
|
or "Tracking started.")
|
||||||
41
resources/commands/time_tracker/stop.lua
Normal file
41
resources/commands/time_tracker/stop.lua
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
-- tracker.stop — close the currently open session.
|
||||||
|
local lang = jarvis.context.language or "ru"
|
||||||
|
local now = jarvis.context.time.timestamp or os.time()
|
||||||
|
|
||||||
|
local data = jarvis.state.get("data") or {}
|
||||||
|
local sessions = data.sessions or {}
|
||||||
|
|
||||||
|
local start_ts = data.current_session_start
|
||||||
|
if not start_ts then
|
||||||
|
return jarvis.cmd.not_found(lang == "ru"
|
||||||
|
and "Трекер не запущен, сэр."
|
||||||
|
or "Tracker isn't running, sir.")
|
||||||
|
end
|
||||||
|
|
||||||
|
local elapsed = now - start_ts
|
||||||
|
if elapsed < 1 then elapsed = 1 end
|
||||||
|
|
||||||
|
table.insert(sessions, { start = start_ts, ["end"] = now })
|
||||||
|
|
||||||
|
data.current_session_start = nil
|
||||||
|
data.sessions = sessions
|
||||||
|
jarvis.state.set("data", data)
|
||||||
|
|
||||||
|
local function format_dur(sec)
|
||||||
|
sec = math.floor(sec)
|
||||||
|
local h = math.floor(sec / 3600)
|
||||||
|
local m = math.floor((sec % 3600) / 60)
|
||||||
|
if lang == "ru" then
|
||||||
|
if h > 0 and m > 0 then return h .. " ч " .. m .. " мин" end
|
||||||
|
if h > 0 then return h .. " ч" end
|
||||||
|
return m .. " мин"
|
||||||
|
else
|
||||||
|
if h > 0 and m > 0 then return h .. "h " .. m .. "m" end
|
||||||
|
if h > 0 then return h .. "h" end
|
||||||
|
return m .. "m"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return jarvis.cmd.ok(lang == "ru"
|
||||||
|
and ("Записал. Сессия: " .. format_dur(elapsed) .. ".")
|
||||||
|
or ("Done. Session: " .. format_dur(elapsed) .. "."))
|
||||||
81
resources/commands/time_tracker/today.lua
Normal file
81
resources/commands/time_tracker/today.lua
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
-- tracker.today — report total active time accrued today (local-day).
|
||||||
|
local lang = jarvis.context.language or "ru"
|
||||||
|
local now = jarvis.context.time.timestamp or os.time()
|
||||||
|
|
||||||
|
local data = jarvis.state.get("data") or {}
|
||||||
|
local sessions = data.sessions or {}
|
||||||
|
|
||||||
|
-- Local-day boundary: use the calendar fields (already in local time).
|
||||||
|
local t = jarvis.context.time
|
||||||
|
local y = tonumber(t.year) or 1970
|
||||||
|
local mo = tonumber(t.month) or 1
|
||||||
|
local d = tonumber(t.day) or 1
|
||||||
|
local h = tonumber(t.hour) or 0
|
||||||
|
local mi = tonumber(t.minute) or 0
|
||||||
|
local s = tonumber(t.second) or 0
|
||||||
|
|
||||||
|
-- Seconds since local midnight today, derived from the broken-down time.
|
||||||
|
local secs_today = h * 3600 + mi * 60 + s
|
||||||
|
local today_start = now - secs_today
|
||||||
|
|
||||||
|
local total = 0
|
||||||
|
for _, sess in ipairs(sessions) do
|
||||||
|
local sstart = tonumber(sess.start) or 0
|
||||||
|
local send = tonumber(sess["end"]) or 0
|
||||||
|
-- Overlap with [today_start, now]
|
||||||
|
local lo = math.max(sstart, today_start)
|
||||||
|
local hi = math.min(send, now)
|
||||||
|
if hi > lo then total = total + (hi - lo) end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Include the open session, if any, up to now.
|
||||||
|
local open_start = data.current_session_start
|
||||||
|
if open_start then
|
||||||
|
local lo = math.max(tonumber(open_start) or 0, today_start)
|
||||||
|
if now > lo then total = total + (now - lo) end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function format_dur(sec)
|
||||||
|
sec = math.floor(sec)
|
||||||
|
local hh = math.floor(sec / 3600)
|
||||||
|
local mm = math.floor((sec % 3600) / 60)
|
||||||
|
if lang == "ru" then
|
||||||
|
local function h_word(n)
|
||||||
|
local n10 = n % 10; local n100 = n % 100
|
||||||
|
if n100 >= 11 and n100 <= 14 then return "часов" end
|
||||||
|
if n10 == 1 then return "час" end
|
||||||
|
if n10 >= 2 and n10 <= 4 then return "часа" end
|
||||||
|
return "часов"
|
||||||
|
end
|
||||||
|
local function m_word(n)
|
||||||
|
local n10 = n % 10; local n100 = n % 100
|
||||||
|
if n100 >= 11 and n100 <= 14 then return "минут" end
|
||||||
|
if n10 == 1 then return "минута" end
|
||||||
|
if n10 >= 2 and n10 <= 4 then return "минуты" end
|
||||||
|
return "минут"
|
||||||
|
end
|
||||||
|
if hh > 0 and mm > 0 then
|
||||||
|
return hh .. " " .. h_word(hh) .. " " .. mm .. " " .. m_word(mm)
|
||||||
|
end
|
||||||
|
if hh > 0 then return hh .. " " .. h_word(hh) end
|
||||||
|
return mm .. " " .. m_word(mm)
|
||||||
|
else
|
||||||
|
if hh > 0 and mm > 0 then return hh .. "h " .. mm .. "m" end
|
||||||
|
if hh > 0 then return hh .. "h" end
|
||||||
|
return mm .. "m"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Suppress unused-warning for y/mo/d — only h/mi/s feed the calc, but having
|
||||||
|
-- y/mo/d on hand keeps the script easy to extend later.
|
||||||
|
local _ = y + mo + d
|
||||||
|
|
||||||
|
if total < 60 then
|
||||||
|
return jarvis.cmd.ok(lang == "ru"
|
||||||
|
and "Сегодня ещё ничего не отработано, сэр."
|
||||||
|
or "Nothing tracked yet today, sir.")
|
||||||
|
end
|
||||||
|
|
||||||
|
return jarvis.cmd.ok(lang == "ru"
|
||||||
|
and ("Сегодня отработано " .. format_dur(total) .. ".")
|
||||||
|
or ("Today: " .. format_dur(total) .. "."))
|
||||||
71
resources/commands/time_tracker/week.lua
Normal file
71
resources/commands/time_tracker/week.lua
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
-- tracker.week — total active time over the last 7 calendar days.
|
||||||
|
local lang = jarvis.context.language or "ru"
|
||||||
|
local now = jarvis.context.time.timestamp or os.time()
|
||||||
|
|
||||||
|
local data = jarvis.state.get("data") or {}
|
||||||
|
local sessions = data.sessions or {}
|
||||||
|
|
||||||
|
-- Local-day boundary today, then back up 6 days to get a 7-day window.
|
||||||
|
local t = jarvis.context.time
|
||||||
|
local h = tonumber(t.hour) or 0
|
||||||
|
local mi = tonumber(t.minute) or 0
|
||||||
|
local s = tonumber(t.second) or 0
|
||||||
|
local secs_today = h * 3600 + mi * 60 + s
|
||||||
|
local today_start = now - secs_today
|
||||||
|
local week_start = today_start - 6 * 86400
|
||||||
|
|
||||||
|
local total = 0
|
||||||
|
for _, sess in ipairs(sessions) do
|
||||||
|
local sstart = tonumber(sess.start) or 0
|
||||||
|
local send = tonumber(sess["end"]) or 0
|
||||||
|
local lo = math.max(sstart, week_start)
|
||||||
|
local hi = math.min(send, now)
|
||||||
|
if hi > lo then total = total + (hi - lo) end
|
||||||
|
end
|
||||||
|
|
||||||
|
local open_start = data.current_session_start
|
||||||
|
if open_start then
|
||||||
|
local lo = math.max(tonumber(open_start) or 0, week_start)
|
||||||
|
if now > lo then total = total + (now - lo) end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function format_dur(sec)
|
||||||
|
sec = math.floor(sec)
|
||||||
|
local hh = math.floor(sec / 3600)
|
||||||
|
local mm = math.floor((sec % 3600) / 60)
|
||||||
|
if lang == "ru" then
|
||||||
|
local function h_word(n)
|
||||||
|
local n10 = n % 10; local n100 = n % 100
|
||||||
|
if n100 >= 11 and n100 <= 14 then return "часов" end
|
||||||
|
if n10 == 1 then return "час" end
|
||||||
|
if n10 >= 2 and n10 <= 4 then return "часа" end
|
||||||
|
return "часов"
|
||||||
|
end
|
||||||
|
local function m_word(n)
|
||||||
|
local n10 = n % 10; local n100 = n % 100
|
||||||
|
if n100 >= 11 and n100 <= 14 then return "минут" end
|
||||||
|
if n10 == 1 then return "минута" end
|
||||||
|
if n10 >= 2 and n10 <= 4 then return "минуты" end
|
||||||
|
return "минут"
|
||||||
|
end
|
||||||
|
if hh > 0 and mm > 0 then
|
||||||
|
return hh .. " " .. h_word(hh) .. " " .. mm .. " " .. m_word(mm)
|
||||||
|
end
|
||||||
|
if hh > 0 then return hh .. " " .. h_word(hh) end
|
||||||
|
return mm .. " " .. m_word(mm)
|
||||||
|
else
|
||||||
|
if hh > 0 and mm > 0 then return hh .. "h " .. mm .. "m" end
|
||||||
|
if hh > 0 then return hh .. "h" end
|
||||||
|
return mm .. "m"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if total < 60 then
|
||||||
|
return jarvis.cmd.ok(lang == "ru"
|
||||||
|
and "За эту неделю ничего не отработано, сэр."
|
||||||
|
or "Nothing tracked this week, sir.")
|
||||||
|
end
|
||||||
|
|
||||||
|
return jarvis.cmd.ok(lang == "ru"
|
||||||
|
and ("За неделю отработано " .. format_dur(total) .. ".")
|
||||||
|
or ("This week: " .. format_dur(total) .. "."))
|
||||||
|
|
@ -3,16 +3,20 @@ local phrase = (jarvis.context.phrase or ""):lower()
|
||||||
|
|
||||||
-- Detect target language from phrase, default English.
|
-- Detect target language from phrase, default English.
|
||||||
local target = "english"
|
local target = "english"
|
||||||
if phrase:find("на русск") then target = "русский"
|
if phrase:find("на русск") then target = "русский"
|
||||||
elseif phrase:find("на украин") then target = "украинский"
|
elseif phrase:find("на немецк") then target = "немецкий"
|
||||||
elseif phrase:find("на немецк") then target = "немецкий"
|
elseif phrase:find("на французск") then target = "французский"
|
||||||
elseif phrase:find("на французск") then target = "французский"
|
elseif phrase:find("на испанск") then target = "испанский"
|
||||||
elseif phrase:find("на испанск") then target = "испанский"
|
elseif phrase:find("на итальянск") then target = "итальянский"
|
||||||
elseif phrase:find("на китайск") then target = "китайский"
|
elseif phrase:find("на польск") then target = "польский"
|
||||||
elseif phrase:find("на японск") then target = "японский"
|
elseif phrase:find("на турецк") then target = "турецкий"
|
||||||
elseif phrase:find("на польск") then target = "польский"
|
elseif phrase:find("на китайск") then target = "китайский"
|
||||||
elseif phrase:find("english") then target = "english"
|
elseif phrase:find("на японск") then target = "японский"
|
||||||
elseif phrase:find("russian") then target = "русский"
|
elseif phrase:find("german") then target = "german"
|
||||||
|
elseif phrase:find("french") then target = "french"
|
||||||
|
elseif phrase:find("spanish") then target = "spanish"
|
||||||
|
elseif phrase:find("english") then target = "english"
|
||||||
|
elseif phrase:find("russian") then target = "русский"
|
||||||
end
|
end
|
||||||
|
|
||||||
local src = jarvis.system.clipboard.get()
|
local src = jarvis.system.clipboard.get()
|
||||||
|
|
|
||||||
|
|
@ -9,21 +9,26 @@ timeout = 20000
|
||||||
ru = [
|
ru = [
|
||||||
"переведи на английский",
|
"переведи на английский",
|
||||||
"переведи на русский",
|
"переведи на русский",
|
||||||
"переведи на украинский",
|
|
||||||
"переведи на немецкий",
|
"переведи на немецкий",
|
||||||
"переведи на французский",
|
"переведи на французский",
|
||||||
"переведи на испанский",
|
"переведи на испанский",
|
||||||
|
"переведи на итальянский",
|
||||||
|
"переведи на польский",
|
||||||
|
"переведи на турецкий",
|
||||||
"переведи на китайский",
|
"переведи на китайский",
|
||||||
"переведи на японский",
|
"переведи на японский",
|
||||||
"переведи",
|
"переведи",
|
||||||
"как по английски",
|
"как по английски",
|
||||||
"как по русски",
|
"как по русски",
|
||||||
"как по немецки",
|
"как по немецки",
|
||||||
|
"как по французски",
|
||||||
]
|
]
|
||||||
en = [
|
en = [
|
||||||
"translate to english",
|
"translate to english",
|
||||||
"translate to russian",
|
"translate to russian",
|
||||||
"translate to ukrainian",
|
"translate to german",
|
||||||
|
"translate to french",
|
||||||
|
"translate to spanish",
|
||||||
"translate",
|
"translate",
|
||||||
"how to say in",
|
"how to say in",
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -2,24 +2,23 @@ local lang = jarvis.context.language
|
||||||
local phrase = (jarvis.context.phrase or ""):lower()
|
local phrase = (jarvis.context.phrase or ""):lower()
|
||||||
|
|
||||||
local target_languages = {
|
local target_languages = {
|
||||||
["английский"] = "English", ["english"] = "English",
|
["английский"] = "English", ["english"] = "English",
|
||||||
["русский"] = "Russian", ["russian"] = "Russian",
|
["русский"] = "Russian", ["russian"] = "Russian",
|
||||||
["украинский"] = "Ukrainian", ["ukrainian"] = "Ukrainian", ["українську"] = "Ukrainian",
|
["немецкий"] = "German", ["german"] = "German",
|
||||||
["немецкий"] = "German", ["german"] = "German", ["німецьку"] = "German",
|
["французский"]= "French", ["french"] = "French",
|
||||||
["французский"]= "French", ["french"] = "French",
|
["испанский"] = "Spanish", ["spanish"] = "Spanish",
|
||||||
["испанский"] = "Spanish", ["spanish"] = "Spanish",
|
["итальянский"]= "Italian", ["italian"] = "Italian",
|
||||||
["итальянский"]= "Italian",
|
["китайский"] = "Chinese", ["chinese"] = "Chinese",
|
||||||
["китайский"] = "Chinese", ["chinese"] = "Chinese",
|
["японский"] = "Japanese", ["japanese"] = "Japanese",
|
||||||
["японский"] = "Japanese", ["japanese"] = "Japanese",
|
["польский"] = "Polish", ["polish"] = "Polish",
|
||||||
["польский"] = "Polish",
|
["турецкий"] = "Turkish", ["turkish"] = "Turkish",
|
||||||
["турецкий"] = "Turkish",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
local triggers_with_lang = {
|
local triggers_with_lang = {
|
||||||
"переведи на ", "translate to ", "переклади на ", "how to say in ",
|
"переведи на ", "translate to ", "how to say in ",
|
||||||
}
|
}
|
||||||
local generic_triggers = {
|
local generic_triggers = {
|
||||||
"переведи", "translate", "переклади", "как по ", "how to say",
|
"переведи", "translate", "как по ", "how to say",
|
||||||
}
|
}
|
||||||
|
|
||||||
local target = nil
|
local target = nil
|
||||||
|
|
@ -112,10 +111,25 @@ jarvis.system.clipboard.set(content)
|
||||||
jarvis.system.notify(target_label, content:sub(1, 200))
|
jarvis.system.notify(target_label, content:sub(1, 200))
|
||||||
jarvis.log("info", "translation: " .. content)
|
jarvis.log("info", "translation: " .. content)
|
||||||
|
|
||||||
|
-- Speak the translation in the TARGET language's voice when possible, so a
|
||||||
|
-- French translation comes out with a French SAPI voice if installed. Falls
|
||||||
|
-- back silently to the default voice if there's no matching locale on the box.
|
||||||
|
local function language_iso(label)
|
||||||
|
if label == "Russian" then return "ru" end
|
||||||
|
if label == "German" then return "de" end
|
||||||
|
if label == "French" then return "fr" end
|
||||||
|
if label == "Spanish" then return "es" end
|
||||||
|
if label == "Italian" then return "it" end
|
||||||
|
if label == "Chinese" then return "zh" end
|
||||||
|
if label == "Japanese" then return "ja" end
|
||||||
|
if label == "Polish" then return "pl" end
|
||||||
|
if label == "Turkish" then return "tr" end
|
||||||
|
return "en"
|
||||||
|
end
|
||||||
local sapi_text = content:gsub("'", "''"):gsub("\r?\n", " ")
|
local sapi_text = content:gsub("'", "''"):gsub("\r?\n", " ")
|
||||||
local ps = string.format(
|
local ps = string.format(
|
||||||
[[Add-Type -AssemblyName System.Speech; $s = New-Object System.Speech.Synthesis.SpeechSynthesizer; $iso='%s'; foreach ($v in $s.GetInstalledVoices()) { if ($v.VoiceInfo.Culture.TwoLetterISOLanguageName -eq $iso) { try { $s.SelectVoice($v.VoiceInfo.Name); break } catch {} } } $s.Speak('%s')]],
|
[[Add-Type -AssemblyName System.Speech; $s = New-Object System.Speech.Synthesis.SpeechSynthesizer; $iso='%s'; foreach ($v in $s.GetInstalledVoices()) { if ($v.VoiceInfo.Culture.TwoLetterISOLanguageName -eq $iso) { try { $s.SelectVoice($v.VoiceInfo.Name); break } catch {} } } $s.Speak('%s')]],
|
||||||
(target_label == "Russian" and "ru") or (target_label == "Ukrainian" and "uk") or "en",
|
language_iso(target_label),
|
||||||
sapi_text
|
sapi_text
|
||||||
)
|
)
|
||||||
jarvis.system.exec(string.format('powershell -NoProfile -ExecutionPolicy Bypass -Command "%s"', ps:gsub('"', '\\"')))
|
jarvis.system.exec(string.format('powershell -NoProfile -ExecutionPolicy Bypass -Command "%s"', ps:gsub('"', '\\"')))
|
||||||
|
|
|
||||||
31
resources/commands/wol/command.toml
Normal file
31
resources/commands/wol/command.toml
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
# Wake-on-LAN — fire a magic packet at a target MAC address. The target is
|
||||||
|
# read from long-term memory under the key "wol_<alias>" so the user can have
|
||||||
|
# multiple machines. Default alias is "server".
|
||||||
|
#
|
||||||
|
# To set a target via voice:
|
||||||
|
# "запомни wol_server AA:BB:CC:DD:EE:FF"
|
||||||
|
# then later: "разбуди сервер"
|
||||||
|
|
||||||
|
[[commands]]
|
||||||
|
id = "wol.wake"
|
||||||
|
type = "lua"
|
||||||
|
script = "wake.lua"
|
||||||
|
sandbox = "full"
|
||||||
|
timeout = 5000
|
||||||
|
|
||||||
|
[commands.phrases]
|
||||||
|
ru = [
|
||||||
|
"разбуди сервер",
|
||||||
|
"включи сервер",
|
||||||
|
"разбуди машину",
|
||||||
|
"разбуди компьютер",
|
||||||
|
"включи компьютер",
|
||||||
|
"wol сервер",
|
||||||
|
]
|
||||||
|
en = [
|
||||||
|
"wake the server",
|
||||||
|
"wake server",
|
||||||
|
"wake on lan",
|
||||||
|
"wol server",
|
||||||
|
"boot the server",
|
||||||
|
]
|
||||||
78
resources/commands/wol/wake.lua
Normal file
78
resources/commands/wol/wake.lua
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
-- Wake-on-LAN — sends a magic packet to a target MAC stored in long-term memory.
|
||||||
|
--
|
||||||
|
-- Setup (one-time, via voice):
|
||||||
|
-- "запомни wol_server AA:BB:CC:DD:EE:FF"
|
||||||
|
-- Then later, anytime:
|
||||||
|
-- "разбуди сервер" → packet goes out to broadcast.
|
||||||
|
--
|
||||||
|
-- Lua can't open raw UDP, so we shell out to PowerShell (System.Net.Sockets).
|
||||||
|
|
||||||
|
local lang = jarvis.context.language
|
||||||
|
local phrase = (jarvis.context.phrase or ""):lower()
|
||||||
|
|
||||||
|
-- Figure out which alias the user wants. Right now we only support "server"
|
||||||
|
-- (the most common case) but the data model already supports more.
|
||||||
|
local alias = "server"
|
||||||
|
|
||||||
|
local mac = jarvis.memory.recall("wol_" .. alias)
|
||||||
|
if not mac or mac == "" then
|
||||||
|
-- Fall back to env so power users can ship a default without touching memory.
|
||||||
|
mac = jarvis.system.env("JARVIS_WOL_TARGET") or ""
|
||||||
|
end
|
||||||
|
|
||||||
|
if mac == "" then
|
||||||
|
return jarvis.cmd.error(lang == "ru"
|
||||||
|
and "Сэр, MAC-адрес не задан. Скажи: запомни wol_server, а потом адрес."
|
||||||
|
or "Sir, MAC address is not set. Say: remember wol_server, then the address.")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Normalise: AA-BB-CC-DD-EE-FF, AA:BB:CC:DD:EE:FF, AABB.CCDD.EEFF, AABBCCDDEEFF
|
||||||
|
local cleaned = mac:gsub("[^%w]", ""):upper()
|
||||||
|
if #cleaned ~= 12 or not cleaned:match("^[0-9A-F]+$") then
|
||||||
|
return jarvis.cmd.error(lang == "ru"
|
||||||
|
and "Сэр, MAC-адрес в памяти выглядит подозрительно: " .. tostring(mac)
|
||||||
|
or "Sir, the stored MAC looks malformed: " .. tostring(mac))
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Inject the cleaned MAC as a constant in the PowerShell script so we don't
|
||||||
|
-- have to argv-escape it. The packet is 6 × 0xFF + 16 × MAC = 102 bytes,
|
||||||
|
-- broadcast on UDP/9.
|
||||||
|
local ps_template = [[
|
||||||
|
$mac = '%s'
|
||||||
|
$macBytes = @()
|
||||||
|
for ($i = 0; $i -lt 12; $i += 2) {
|
||||||
|
$macBytes += [Convert]::ToByte($mac.Substring($i, 2), 16)
|
||||||
|
}
|
||||||
|
$packet = New-Object byte[] 102
|
||||||
|
for ($i = 0; $i -lt 6; $i++) { $packet[$i] = 0xFF }
|
||||||
|
for ($r = 0; $r -lt 16; $r++) {
|
||||||
|
for ($j = 0; $j -lt 6; $j++) {
|
||||||
|
$packet[6 + $r * 6 + $j] = $macBytes[$j]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$udp = New-Object System.Net.Sockets.UdpClient
|
||||||
|
$udp.EnableBroadcast = $true
|
||||||
|
$null = $udp.Send($packet, $packet.Length, '255.255.255.255', 9)
|
||||||
|
$udp.Close()
|
||||||
|
Write-Output 'sent'
|
||||||
|
]]
|
||||||
|
|
||||||
|
local ps = string.format(ps_template, cleaned)
|
||||||
|
local cmd = 'powershell -NoProfile -ExecutionPolicy Bypass -Command "& {'
|
||||||
|
.. ps:gsub('\r?\n', '; '):gsub('"', '\\"')
|
||||||
|
.. '}"'
|
||||||
|
|
||||||
|
jarvis.log("info", "WOL packet → " .. cleaned)
|
||||||
|
local res = jarvis.system.exec(cmd)
|
||||||
|
if not res.success then
|
||||||
|
jarvis.log("error", "WOL failed: " .. tostring(res.stderr))
|
||||||
|
return jarvis.cmd.error(lang == "ru"
|
||||||
|
and "Не получилось отправить пакет."
|
||||||
|
or "Failed to send packet.")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Format the MAC back for speech: "AA-BB-CC-..." sounds clearer than "AABB..."
|
||||||
|
local pretty = cleaned:gsub("(%w%w)", "%1-"):sub(1, -2)
|
||||||
|
return jarvis.cmd.ok(lang == "ru"
|
||||||
|
and ("Пакет отправлен на " .. pretty)
|
||||||
|
or ("Packet sent to " .. pretty))
|
||||||
Loading…
Add table
Add a link
Reference in a new issue