arch + features: Lua jarvis.speak() / jarvis.llm() API, /commands GUI page, games / mouse / random_choice packs

Architecture — DRY at the Lua API surface:

  lua/api/tts.rs (new): jarvis.speak(text, opts?). opts.lang = ISO
  two-letter code (default "ru"); auto-picks first installed voice
  matching the culture. opts.async = true|false (default true). The
  9 packs that previously inlined a 60-character PS-SAPI string for
  every spoken reply can now reduce to one line. Refactored fun/,
  dice/, clipboard_read/ as proof — fun/ask.lua went from 75 lines
  of HTTP+SAPI boilerplate to ~28 lines of intent code.

  lua/api/llm.rs (new): jarvis.llm(messages, opts?). messages is a
  list of {role, content} tables, opts.max_tokens / .temperature /
  .top_p. Returns the assistant string or nil. Gated by sandbox
  allows_http() (so Standard+ packs only). Internally uses the
  existing jarvis_core::llm::LlmClient::complete_with, no duplicated
  HTTP plumbing.

  llm/client.rs: split complete() into complete_with(messages,
  max_tokens, temperature, top_p) for the new caller; old complete()
  is now a thin wrapper at temp=0.7 top_p=1.0 for backward compat.

GUI — /commands page rewrite:

  Replaces the "Раздел в разработке" placeholder. Calls Tauri command
  get_commands_list (already existed in tauri_commands/commands.rs,
  was wired but unused). Renders a search-filterable card grid:
    - cmd id (monospace)
    - type badge with colour by kind (lua=blue, ahk=red, cli=grey,
      voice=green, terminate=red, stop_chaining=violet)
    - sandbox badge
    - description line (if non-empty)
    - all phrases for currentLanguage, fallback to en, fallback to
      first available; each phrase in a small inline pill
  Toolbar: text input with magnifier icon + reload button (↻).
  Counter line shows visible/total + "Скажи Джарвис + любую фразу"
  hint. Counts down as you type the filter.

New packs (3, total 33 → 36):

  games/ — launch_game / list_games. Reads or creates
  %USERPROFILE%\Documents\jarvis-games.json (the sample preloads dota,
  cs2, elden ring, witcher 3, factorio + minecraft launcher path +
  fortnite epic URI). Trigger-strip + fuzzy name/alias match, then
  launches via steam://rungameid/N (Steam), epic:// (Epic Games), or
  start "" "path" (anything else). list_games speaks the configured
  library.

  mouse/ — left/right/middle/double click + scroll up/down. Single
  dispatch.lua + _mouse_helper.ps1. PS P/Invokes user32!mouse_event
  with the right flag pair (LEFTDOWN+LEFTUP / etc.), 30ms gap, doubles
  do two LEFT cycles with a 50ms gap, wheel uses ±360 ticks.

  random_choice/ — "выбери из X или Y или Z". Strips the trigger,
  splits on " или " / " or " / ", " / " либо " / " чи ", math.random
  picks one, speaks "Я выбираю N".

cargo test commands::tests still 3/3. 36 packs verified via
jarvis-gui startup log.
This commit is contained in:
Bossiara13 2026-05-15 12:58:16 +03:00
parent 9d8b917149
commit a16d2401e7
17 changed files with 765 additions and 83 deletions

View file

@ -16,55 +16,24 @@ local prompts = {
or "Compliment the user briefly, in JARVIS-the-butler tone.",
}
local sys = prompts[cmd_id] or prompts.joke
local token = jarvis.system.env("GROQ_TOKEN")
if not token or token == "" then
jarvis.system.notify("Fun", "GROQ_TOKEN не задан")
jarvis.audio.play_error()
return { chain = false }
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
math.randomseed(os.time() + jarvis.context.time.second * 7919)
local seed_word = (math.random(1, 9999)) .. ""
local payload = {
model = model,
messages = {
{ role = "system", content = sys },
{ role = "user", content = "seed " .. seed_word },
local reply = jarvis.llm(
{
{ role = "system", content = prompts[cmd_id] or prompts.joke },
{ role = "user", content = "seed " .. tostring(math.random(1, 9999)) },
},
max_tokens = 220,
temperature = 1.0,
}
local res = jarvis.http.post_json(base .. "/chat/completions", payload, { Authorization = "Bearer " .. token })
{ max_tokens = 220, temperature = 1.0 }
)
if not res.ok then
jarvis.system.notify("Fun", "Groq не ответил")
if not reply or reply == "" then
jarvis.system.notify("Fun", lang == "ru" and "Не получилось" or "Failed")
jarvis.audio.play_error()
return { chain = false }
end
local content = (res.body or ""):match('"content"%s*:%s*"(.-[^\\])"')
if not content then
jarvis.audio.play_error()
return { chain = false }
end
content = content:gsub('\\n', ' '):gsub('\\"', '"'):gsub('\\\\', '\\'):gsub("%s+", " ")
content = content:gsub("^%s+", ""):gsub("%s+$", "")
local titles = { joke = "Анекдот", fact = "Факт", quote = "Цитата", compliment = "Комплимент" }
jarvis.system.notify(titles[cmd_id] or "Fun", content:sub(1, 240))
local sapi = content: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.system.notify(titles[cmd_id] or "Fun", reply:sub(1, 240))
jarvis.speak(reply, { lang = lang })
jarvis.audio.play_ok()
return { chain = false }