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:
parent
9d8b917149
commit
a16d2401e7
17 changed files with 765 additions and 83 deletions
|
|
@ -103,13 +103,23 @@ impl LlmClient {
|
|||
}
|
||||
|
||||
pub fn complete(&self, messages: &[ChatMessage], max_tokens: u32) -> Result<String, LlmError> {
|
||||
self.complete_with(messages, max_tokens, 0.7, 1.0)
|
||||
}
|
||||
|
||||
pub fn complete_with(
|
||||
&self,
|
||||
messages: &[ChatMessage],
|
||||
max_tokens: u32,
|
||||
temperature: f32,
|
||||
top_p: f32,
|
||||
) -> Result<String, LlmError> {
|
||||
let url = format!("{}/chat/completions", self.base_url.trim_end_matches('/'));
|
||||
let body = ChatRequest {
|
||||
model: &self.model,
|
||||
messages,
|
||||
max_tokens,
|
||||
temperature: 0.7,
|
||||
top_p: 1.0,
|
||||
temperature,
|
||||
top_p,
|
||||
};
|
||||
|
||||
let resp = self
|
||||
|
|
|
|||
|
|
@ -4,4 +4,6 @@ pub mod context;
|
|||
pub mod http;
|
||||
pub mod fs;
|
||||
pub mod state;
|
||||
pub mod system;
|
||||
pub mod system;
|
||||
pub mod tts;
|
||||
pub mod llm;
|
||||
64
crates/jarvis-core/src/lua/api/llm.rs
Normal file
64
crates/jarvis-core/src/lua/api/llm.rs
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
use mlua::{Lua, Table, Value};
|
||||
|
||||
use crate::llm::{ChatMessage, LlmClient};
|
||||
|
||||
// Lua-callable wrapper around jarvis_core::llm::LlmClient.
|
||||
// Centralises the Groq plumbing so command scripts can just say:
|
||||
// local reply = jarvis.llm(
|
||||
// { {role='system', content='...'}, {role='user', content='...'} },
|
||||
// { temperature = 0.0, max_tokens = 64 }
|
||||
// )
|
||||
// Returns the assistant text (string) or nil on any failure (missing token,
|
||||
// network error, empty response, etc.) — callers should always check `if reply`.
|
||||
pub fn register(lua: &Lua, jarvis: &Table) -> mlua::Result<()> {
|
||||
let chat_fn = lua.create_function(|lua, (messages, opts): (Table, Option<Table>)| {
|
||||
let mut chat_messages = Vec::new();
|
||||
for pair in messages.sequence_values::<Table>() {
|
||||
let msg = pair?;
|
||||
let role = msg.get::<String>("role").unwrap_or_else(|_| "user".to_string());
|
||||
let content = msg.get::<String>("content").unwrap_or_default();
|
||||
if content.is_empty() {
|
||||
continue;
|
||||
}
|
||||
chat_messages.push(match role.as_str() {
|
||||
"system" => ChatMessage::system(content),
|
||||
"assistant" => ChatMessage::assistant(content),
|
||||
_ => ChatMessage::user(content),
|
||||
});
|
||||
}
|
||||
if chat_messages.is_empty() {
|
||||
return Ok(Value::Nil);
|
||||
}
|
||||
|
||||
let mut max_tokens: u32 = 256;
|
||||
let mut temperature: f32 = 0.7;
|
||||
let mut top_p: f32 = 1.0;
|
||||
if let Some(o) = opts {
|
||||
if let Ok(v) = o.get::<u32>("max_tokens") { max_tokens = v; }
|
||||
if let Ok(v) = o.get::<f32>("temperature") { temperature = v; }
|
||||
if let Ok(v) = o.get::<f32>("top_p") { top_p = v; }
|
||||
}
|
||||
|
||||
let client = match LlmClient::from_env() {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
log::warn!("[Lua llm] no client: {}", e);
|
||||
return Ok(Value::Nil);
|
||||
}
|
||||
};
|
||||
|
||||
match client.complete_with(&chat_messages, max_tokens, temperature, top_p) {
|
||||
Ok(reply) => {
|
||||
let s = lua.create_string(reply.trim())?;
|
||||
Ok(Value::String(s))
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("[Lua llm] request failed: {}", e);
|
||||
Ok(Value::Nil)
|
||||
}
|
||||
}
|
||||
})?;
|
||||
|
||||
jarvis.set("llm", chat_fn)?;
|
||||
Ok(())
|
||||
}
|
||||
63
crates/jarvis-core/src/lua/api/tts.rs
Normal file
63
crates/jarvis-core/src/lua/api/tts.rs
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
use mlua::{Lua, Table};
|
||||
use std::process::{Command, Stdio};
|
||||
|
||||
pub fn register(lua: &Lua, jarvis: &Table) -> mlua::Result<()> {
|
||||
// jarvis.speak(text, opts?)
|
||||
// opts.lang: ISO 2-letter code (ru, en, de, ...). default "ru"
|
||||
// opts.async: if true, fire-and-forget; if false, block until done. default true
|
||||
let speak_fn = lua.create_function(|_, (text, opts): (String, Option<Table>)| {
|
||||
let mut iso = "ru".to_string();
|
||||
let mut detached = true;
|
||||
|
||||
if let Some(t) = opts {
|
||||
if let Ok(v) = t.get::<String>("lang") { iso = v; }
|
||||
if let Ok(v) = t.get::<bool>("async") { detached = v; }
|
||||
}
|
||||
|
||||
speak_via_sapi(&text, &iso, detached);
|
||||
Ok(())
|
||||
})?;
|
||||
jarvis.set("speak", speak_fn)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn speak_via_sapi(text: &str, iso: &str, detached: bool) {
|
||||
if text.is_empty() {
|
||||
return;
|
||||
}
|
||||
let escaped = text.replace('\'', "''").replace('\r', " ").replace('\n', " ");
|
||||
let safe_iso = if iso.chars().all(|c| c.is_ascii_alphabetic()) && iso.len() == 2 {
|
||||
iso.to_lowercase()
|
||||
} else {
|
||||
"ru".to_string()
|
||||
};
|
||||
let ps = format!(
|
||||
"Add-Type -AssemblyName System.Speech; \
|
||||
$s = New-Object System.Speech.Synthesis.SpeechSynthesizer; \
|
||||
foreach ($v in $s.GetInstalledVoices()) {{ \
|
||||
if ($v.VoiceInfo.Culture.TwoLetterISOLanguageName -eq '{}') {{ \
|
||||
try {{ $s.SelectVoice($v.VoiceInfo.Name); break }} catch {{}} \
|
||||
}} \
|
||||
}} \
|
||||
$s.Speak('{}')",
|
||||
safe_iso, escaped
|
||||
);
|
||||
|
||||
let mut cmd = Command::new("powershell");
|
||||
cmd.args(["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", &ps])
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null());
|
||||
|
||||
if detached {
|
||||
let _ = cmd.spawn();
|
||||
} else {
|
||||
let _ = cmd.status();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
fn speak_via_sapi(text: &str, _iso: &str, _detached: bool) {
|
||||
log::info!("[Lua tts] would speak: {}", text);
|
||||
}
|
||||
|
|
@ -76,10 +76,12 @@ impl LuaEngine {
|
|||
api::core::register(&self.lua, &jarvis)?;
|
||||
api::audio::register(&self.lua, &jarvis)?;
|
||||
api::context::register(&self.lua, &jarvis, context)?;
|
||||
|
||||
api::tts::register(&self.lua, &jarvis)?;
|
||||
|
||||
// sandbox-controlled APIs
|
||||
if self.sandbox.allows_http() {
|
||||
api::http::register(&self.lua, &jarvis)?;
|
||||
api::llm::register(&self.lua, &jarvis)?;
|
||||
}
|
||||
|
||||
if self.sandbox.allows_state() {
|
||||
|
|
|
|||
|
|
@ -1,35 +1,249 @@
|
|||
<script lang="ts">
|
||||
import { Notification, Space } from "@svelteuidev/core"
|
||||
import { InfoCircled } from "radix-icons-svelte"
|
||||
import { onMount } from "svelte"
|
||||
import { TextInput, Space, Badge, Tooltip, Loader } from "@svelteuidev/core"
|
||||
import { invoke } from "@tauri-apps/api/core"
|
||||
import { MagnifyingGlass } from "radix-icons-svelte"
|
||||
|
||||
import HDivider from "@/components/elements/HDivider.svelte"
|
||||
import Footer from "@/components/Footer.svelte"
|
||||
import { appInfo, translations, translate } from "@/stores"
|
||||
import { currentLanguage, translations, translate } from "@/stores"
|
||||
|
||||
$: t = (key: string) => translate($translations, key)
|
||||
|
||||
let pythonForkLink = ""
|
||||
let repoLink = ""
|
||||
appInfo.subscribe(info => {
|
||||
pythonForkLink = info.pythonForkLink
|
||||
repoLink = info.repositoryLink
|
||||
})
|
||||
type JCommand = {
|
||||
id: string
|
||||
type: string
|
||||
description?: string
|
||||
script?: string
|
||||
sandbox?: string
|
||||
timeout?: number
|
||||
phrases: Record<string, string[]>
|
||||
sounds?: Record<string, string[]>
|
||||
}
|
||||
|
||||
let commands: JCommand[] = []
|
||||
let loading = true
|
||||
let filter = ""
|
||||
let errorMessage = ""
|
||||
|
||||
async function load() {
|
||||
loading = true
|
||||
errorMessage = ""
|
||||
try {
|
||||
commands = await invoke<JCommand[]>("get_commands_list")
|
||||
commands = [...commands].sort((a, b) => a.id.localeCompare(b.id))
|
||||
} catch (err) {
|
||||
errorMessage = String(err)
|
||||
console.error("commands list load failed:", err)
|
||||
} finally {
|
||||
loading = false
|
||||
}
|
||||
}
|
||||
|
||||
onMount(load)
|
||||
|
||||
function phrasesForLang(c: JCommand, lang: string): string[] {
|
||||
return c.phrases?.[lang] ?? c.phrases?.["en"] ?? Object.values(c.phrases ?? {})[0] ?? []
|
||||
}
|
||||
|
||||
function matches(c: JCommand, q: string): boolean {
|
||||
if (!q) return true
|
||||
const needle = q.toLowerCase()
|
||||
if (c.id.toLowerCase().includes(needle)) return true
|
||||
if ((c.description ?? "").toLowerCase().includes(needle)) return true
|
||||
if ((c.type ?? "").toLowerCase().includes(needle)) return true
|
||||
for (const list of Object.values(c.phrases ?? {})) {
|
||||
for (const p of list) {
|
||||
if (p.toLowerCase().includes(needle)) return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function typeColor(type: string): string {
|
||||
switch (type) {
|
||||
case "lua": return "#5b8def"
|
||||
case "ahk": return "#b7411a"
|
||||
case "cli": return "#8a8d90"
|
||||
case "voice": return "#3aaf72"
|
||||
case "terminate": return "#c93a3a"
|
||||
case "stop_chaining": return "#9d6abf"
|
||||
default: return "#6c6e71"
|
||||
}
|
||||
}
|
||||
|
||||
$: visible = commands.filter(c => matches(c, filter))
|
||||
$: countLabel = filter
|
||||
? `${visible.length} / ${commands.length}`
|
||||
: `${commands.length}`
|
||||
</script>
|
||||
|
||||
<Space h="xl" />
|
||||
<Space h="md" />
|
||||
|
||||
<Notification
|
||||
title={t('commands-wip-title')}
|
||||
icon={InfoCircled}
|
||||
color="blue"
|
||||
withCloseButton={false}
|
||||
>
|
||||
{t('commands-wip-desc')}<br />
|
||||
{t('commands-wip-builder')}
|
||||
<a href={pythonForkLink} target="_blank" rel="noopener noreferrer">{t('commands-wip-builder-link')}</a>.
|
||||
{t('commands-wip-or-fork')}
|
||||
<a href={repoLink} target="_blank" rel="noopener noreferrer">{t('commands-wip-fork-link')}</a>.
|
||||
</Notification>
|
||||
<div class="toolbar">
|
||||
<TextInput
|
||||
bind:value={filter}
|
||||
placeholder={$currentLanguage === "ru" ? "Поиск по имени или фразе..." : "Search by id or phrase..."}
|
||||
icon={MagnifyingGlass}
|
||||
size="sm"
|
||||
radius="md"
|
||||
override={{ flex: 1 }}
|
||||
/>
|
||||
<Tooltip label={$currentLanguage === "ru" ? "Перечитать" : "Reload"} position="bottom">
|
||||
<button class="reload-btn" on:click={load} aria-label="reload">↻</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div class="meta">
|
||||
<span class="count">{countLabel}</span>
|
||||
<span class="dot">·</span>
|
||||
<span class="hint">
|
||||
{$currentLanguage === "ru"
|
||||
? "Скажи «Джарвис» + любую фразу из карточки"
|
||||
: "Say \"Jarvis\" + any phrase from a card"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{#if loading}
|
||||
<div class="state"><Loader size="sm" /></div>
|
||||
{:else if errorMessage}
|
||||
<div class="state error">{errorMessage}</div>
|
||||
{:else if visible.length === 0}
|
||||
<div class="state">
|
||||
{$currentLanguage === "ru" ? "Ничего не найдено" : "Nothing found"}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="grid">
|
||||
{#each visible as cmd (cmd.id)}
|
||||
<div class="card">
|
||||
<div class="card-head">
|
||||
<span class="cmd-id">{cmd.id}</span>
|
||||
<Badge
|
||||
size="xs"
|
||||
radius="sm"
|
||||
variant="filled"
|
||||
override={{ backgroundColor: typeColor(cmd.type), color: "#fff" }}
|
||||
>
|
||||
{cmd.type}
|
||||
</Badge>
|
||||
{#if cmd.sandbox}
|
||||
<Badge size="xs" radius="sm" variant="outline">{cmd.sandbox}</Badge>
|
||||
{/if}
|
||||
</div>
|
||||
{#if cmd.description}
|
||||
<div class="card-desc">{cmd.description}</div>
|
||||
{/if}
|
||||
<ul class="phrases">
|
||||
{#each phrasesForLang(cmd, $currentLanguage) as p}
|
||||
<li>«{p}»</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<HDivider />
|
||||
<Footer />
|
||||
|
||||
<style lang="scss">
|
||||
.toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 0 10px;
|
||||
}
|
||||
.reload-btn {
|
||||
background: transparent;
|
||||
border: 1px solid #3a3d40;
|
||||
color: #c8cacc;
|
||||
border-radius: 8px;
|
||||
height: 34px;
|
||||
width: 34px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: 0.2s;
|
||||
&:hover {
|
||||
border-color: #5b8def;
|
||||
color: #5b8def;
|
||||
}
|
||||
}
|
||||
.meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 12px 12px;
|
||||
font-size: 12px;
|
||||
color: #8a8d90;
|
||||
.count {
|
||||
background: #5b8def;
|
||||
color: #fff;
|
||||
padding: 1px 8px;
|
||||
border-radius: 10px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.dot { color: #46484b; }
|
||||
}
|
||||
.state {
|
||||
padding: 30px 12px;
|
||||
text-align: center;
|
||||
color: #8a8d90;
|
||||
font-size: 13px;
|
||||
&.error {
|
||||
color: #c93a3a;
|
||||
font-family: monospace;
|
||||
}
|
||||
}
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 8px;
|
||||
padding: 0 10px 10px;
|
||||
}
|
||||
.card {
|
||||
background: #1c1d20;
|
||||
border: 1px solid #2a2c2f;
|
||||
border-radius: 8px;
|
||||
padding: 10px 12px;
|
||||
transition: border-color 0.15s;
|
||||
&:hover {
|
||||
border-color: #3a3d40;
|
||||
}
|
||||
}
|
||||
.card-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-bottom: 4px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.cmd-id {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
font-size: 12px;
|
||||
color: #e0e2e4;
|
||||
font-weight: 600;
|
||||
margin-right: auto;
|
||||
}
|
||||
.card-desc {
|
||||
font-size: 12px;
|
||||
color: #a0a3a6;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.phrases {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-size: 12px;
|
||||
color: #c8cacc;
|
||||
line-height: 1.6em;
|
||||
li {
|
||||
display: inline-block;
|
||||
background: #25272a;
|
||||
border-radius: 4px;
|
||||
padding: 1px 7px;
|
||||
margin: 2px 4px 2px 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -17,17 +17,10 @@ if #to_speak > preview_chars then
|
|||
to_speak = to_speak:sub(1, preview_chars) .. (lang == "ru" and " ... и так далее" or " ... and so on")
|
||||
end
|
||||
|
||||
local sapi_text = to_speak: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_text
|
||||
)
|
||||
local cmd = string.format('powershell -NoProfile -ExecutionPolicy Bypass -Command "%s"', ps:gsub('"', '\\"'))
|
||||
jarvis.system.exec(cmd)
|
||||
|
||||
jarvis.system.notify(
|
||||
lang == "ru" and "Буфер" or "Clipboard",
|
||||
text:sub(1, 120)
|
||||
)
|
||||
jarvis.speak(to_speak, { lang = lang })
|
||||
jarvis.audio.play_ok()
|
||||
return { chain = false }
|
||||
|
|
|
|||
|
|
@ -23,13 +23,6 @@ else
|
|||
end
|
||||
|
||||
jarvis.system.notify(title, text)
|
||||
|
||||
local sapi = say:gsub("'", "''")
|
||||
local sps = 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"', sps:gsub('"', '\\"')))
|
||||
|
||||
jarvis.speak(say, { lang = lang })
|
||||
jarvis.audio.play_ok()
|
||||
return { chain = false }
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
41
resources/commands/games/command.toml
Normal file
41
resources/commands/games/command.toml
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
[[commands]]
|
||||
id = "launch_game"
|
||||
type = "lua"
|
||||
script = "launch.lua"
|
||||
sandbox = "full"
|
||||
timeout = 5000
|
||||
|
||||
[commands.phrases]
|
||||
ru = [
|
||||
"запусти игру",
|
||||
"включи игру",
|
||||
"поиграем",
|
||||
"хочу поиграть",
|
||||
"запусти доту",
|
||||
"включи кс",
|
||||
"включи фортнайт",
|
||||
"включи майнкрафт",
|
||||
]
|
||||
en = [
|
||||
"launch game",
|
||||
"play game",
|
||||
"launch dota",
|
||||
"launch cs",
|
||||
]
|
||||
ua = [
|
||||
"запусти гру",
|
||||
"увімкни гру",
|
||||
]
|
||||
|
||||
|
||||
[[commands]]
|
||||
id = "list_games"
|
||||
type = "lua"
|
||||
script = "list.lua"
|
||||
sandbox = "full"
|
||||
timeout = 3000
|
||||
|
||||
[commands.phrases]
|
||||
ru = ["какие у меня игры", "список игр", "покажи игры"]
|
||||
en = ["list games", "show games"]
|
||||
ua = ["які в мене ігри"]
|
||||
110
resources/commands/games/launch.lua
Normal file
110
resources/commands/games/launch.lua
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
local lang = jarvis.context.language
|
||||
local userprofile = jarvis.system.env("USERPROFILE") or "C:\\Users\\Public"
|
||||
local config_path = userprofile .. "\\Documents\\jarvis-games.json"
|
||||
|
||||
if not jarvis.fs.exists(config_path) then
|
||||
local sample = [[[
|
||||
{"name": "dota", "aliases": ["доту", "доту 2", "dota 2"], "steam_appid": 570},
|
||||
{"name": "cs2", "aliases": ["кс", "контру", "контр страйк"], "steam_appid": 730},
|
||||
{"name": "elden ring", "aliases": ["элден", "элден ринг"], "steam_appid": 1245620},
|
||||
{"name": "witcher 3", "aliases": ["ведьмак", "ведьмака 3"], "steam_appid": 292030},
|
||||
{"name": "factorio", "aliases": ["факторио"], "steam_appid": 427520},
|
||||
{"name": "minecraft", "aliases": ["майнкрафт", "майн"], "path": "C:\\Program Files (x86)\\Minecraft Launcher\\MinecraftLauncher.exe"},
|
||||
{"name": "fortnite", "aliases": ["фортнайт"], "epic_uri": "com.epicgames.launcher://apps/Fortnite?action=launch&silent=true"}
|
||||
]
|
||||
]]
|
||||
jarvis.fs.write(config_path, sample)
|
||||
jarvis.system.notify(
|
||||
lang == "ru" and "Игры" or "Games",
|
||||
lang == "ru" and "Создан шаблон ~/Documents/jarvis-games.json — добавь свои игры и повтори"
|
||||
or "Template at Documents/jarvis-games.json — edit and retry"
|
||||
)
|
||||
jarvis.system.open(config_path)
|
||||
jarvis.audio.play_not_found()
|
||||
return { chain = false }
|
||||
end
|
||||
|
||||
local content = jarvis.fs.read(config_path)
|
||||
local entries = {}
|
||||
local cursor = 1
|
||||
while true do
|
||||
local s, e, block = content:find("({[^{}]+})", cursor)
|
||||
if not s then break end
|
||||
cursor = e + 1
|
||||
local name = block:match('"name"%s*:%s*"([^"]+)"')
|
||||
if name then
|
||||
local entry = { name = name, name_lower = name:lower(), aliases = {} }
|
||||
for alias in block:gmatch('"([^"]+)"') do
|
||||
entry.aliases[#entry.aliases + 1] = alias:lower()
|
||||
end
|
||||
entry.steam_appid = tonumber(block:match('"steam_appid"%s*:%s*(%d+)'))
|
||||
entry.path = block:match('"path"%s*:%s*"([^"]+)"')
|
||||
entry.epic_uri = block:match('"epic_uri"%s*:%s*"([^"]+)"')
|
||||
table.insert(entries, entry)
|
||||
end
|
||||
end
|
||||
|
||||
if #entries == 0 then
|
||||
jarvis.system.notify("Games", lang == "ru" and "Пусто в конфиге" or "Empty config")
|
||||
jarvis.audio.play_not_found()
|
||||
return { chain = false }
|
||||
end
|
||||
|
||||
local phrase = (jarvis.context.phrase or ""):lower()
|
||||
local triggers = {
|
||||
"запусти игру", "включи игру", "хочу поиграть", "поиграем", "запусти", "включи",
|
||||
"launch game", "play game", "launch",
|
||||
"запусти гру", "увімкни гру",
|
||||
}
|
||||
local query = phrase
|
||||
for _, t in ipairs(triggers) do
|
||||
local s, e = string.find(query, t, 1, true)
|
||||
if s == 1 then
|
||||
query = query:sub(e + 1)
|
||||
break
|
||||
end
|
||||
end
|
||||
query = query:gsub("^%s+", ""):gsub("%s+$", "")
|
||||
|
||||
local function name_matches(e, q)
|
||||
if e.name_lower == q then return true end
|
||||
if string.find(e.name_lower, q, 1, true) then return true end
|
||||
for _, a in ipairs(e.aliases) do
|
||||
if a == q or string.find(q, a, 1, true) then return true end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local match
|
||||
for _, e in ipairs(entries) do
|
||||
if name_matches(e, query) then match = e; break end
|
||||
end
|
||||
|
||||
if not match then
|
||||
jarvis.system.notify("Games", (lang == "ru" and "Не нашёл: " or "Not found: ") .. query)
|
||||
jarvis.audio.play_not_found()
|
||||
return { chain = false }
|
||||
end
|
||||
|
||||
local launched = false
|
||||
if match.steam_appid then
|
||||
jarvis.system.open("steam://rungameid/" .. tostring(match.steam_appid))
|
||||
launched = true
|
||||
elseif match.epic_uri then
|
||||
jarvis.system.open(match.epic_uri)
|
||||
launched = true
|
||||
elseif match.path and jarvis.fs.exists(match.path) then
|
||||
jarvis.system.exec(string.format('start "" "%s"', match.path))
|
||||
launched = true
|
||||
end
|
||||
|
||||
if launched then
|
||||
jarvis.system.notify(lang == "ru" and "Запускаю" or "Launching", match.name)
|
||||
jarvis.speak((lang == "ru" and "Запускаю " or "Launching ") .. match.name .. ".", { lang = lang })
|
||||
jarvis.audio.play_ok()
|
||||
else
|
||||
jarvis.system.notify("Games", (lang == "ru" and "Не настроен запуск: " or "No launch info: ") .. match.name)
|
||||
jarvis.audio.play_error()
|
||||
end
|
||||
|
||||
return { chain = false }
|
||||
26
resources/commands/games/list.lua
Normal file
26
resources/commands/games/list.lua
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
local lang = jarvis.context.language
|
||||
local userprofile = jarvis.system.env("USERPROFILE") or "C:\\Users\\Public"
|
||||
local config_path = userprofile .. "\\Documents\\jarvis-games.json"
|
||||
|
||||
if not jarvis.fs.exists(config_path) then
|
||||
jarvis.system.notify("Games", lang == "ru"
|
||||
and "Конфиг не создан — скажи «запусти игру» один раз" or "No config yet")
|
||||
jarvis.audio.play_not_found()
|
||||
return { chain = false }
|
||||
end
|
||||
|
||||
local names = {}
|
||||
for name in jarvis.fs.read(config_path):gmatch('"name"%s*:%s*"([^"]+)"') do
|
||||
table.insert(names, name)
|
||||
end
|
||||
if #names == 0 then
|
||||
jarvis.system.notify("Games", "Empty")
|
||||
jarvis.audio.play_not_found()
|
||||
return { chain = false }
|
||||
end
|
||||
|
||||
local body = table.concat(names, ", ")
|
||||
jarvis.system.notify((lang == "ru" and "Игр: " or "Games: ") .. #names, body)
|
||||
jarvis.speak((lang == "ru" and "В библиотеке: " or "Library: ") .. body, { lang = lang })
|
||||
jarvis.audio.play_ok()
|
||||
return { chain = false }
|
||||
33
resources/commands/mouse/_mouse_helper.ps1
Normal file
33
resources/commands/mouse/_mouse_helper.ps1
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
param(
|
||||
[Parameter(Mandatory=$true)]
|
||||
[ValidateSet("left","right","middle","double","scroll_up","scroll_down")]
|
||||
[string]$Action
|
||||
)
|
||||
|
||||
Add-Type -Name MouseM -Namespace Win32 -MemberDefinition @'
|
||||
[DllImport("user32.dll")]
|
||||
public static extern void mouse_event(uint flags, int dx, int dy, int data, System.UIntPtr extra);
|
||||
'@
|
||||
|
||||
$LEFTDOWN = 0x0002
|
||||
$LEFTUP = 0x0004
|
||||
$RIGHTDOWN = 0x0008
|
||||
$RIGHTUP = 0x0010
|
||||
$MIDDOWN = 0x0020
|
||||
$MIDUP = 0x0040
|
||||
$WHEEL = 0x0800
|
||||
|
||||
function Click([uint32]$down, [uint32]$up) {
|
||||
[Win32.MouseM]::mouse_event($down, 0, 0, 0, [UIntPtr]::Zero)
|
||||
Start-Sleep -Milliseconds 30
|
||||
[Win32.MouseM]::mouse_event($up, 0, 0, 0, [UIntPtr]::Zero)
|
||||
}
|
||||
|
||||
switch ($Action) {
|
||||
"left" { Click $LEFTDOWN $LEFTUP }
|
||||
"right" { Click $RIGHTDOWN $RIGHTUP }
|
||||
"middle" { Click $MIDDOWN $MIDUP }
|
||||
"double" { Click $LEFTDOWN $LEFTUP; Start-Sleep -Milliseconds 50; Click $LEFTDOWN $LEFTUP }
|
||||
"scroll_up" { [Win32.MouseM]::mouse_event($WHEEL, 0, 0, 360, [UIntPtr]::Zero) }
|
||||
"scroll_down" { [Win32.MouseM]::mouse_event($WHEEL, 0, 0, -360, [UIntPtr]::Zero) }
|
||||
}
|
||||
63
resources/commands/mouse/command.toml
Normal file
63
resources/commands/mouse/command.toml
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
[[commands]]
|
||||
id = "mouse_left_click"
|
||||
type = "lua"
|
||||
script = "dispatch.lua"
|
||||
sandbox = "full"
|
||||
timeout = 3000
|
||||
|
||||
[commands.phrases]
|
||||
ru = ["клик", "кликни", "нажми мышкой"]
|
||||
en = ["click", "left click"]
|
||||
ua = ["клік", "клікни"]
|
||||
|
||||
|
||||
[[commands]]
|
||||
id = "mouse_right_click"
|
||||
type = "lua"
|
||||
script = "dispatch.lua"
|
||||
sandbox = "full"
|
||||
timeout = 3000
|
||||
|
||||
[commands.phrases]
|
||||
ru = ["правый клик", "клик правой", "правой кнопкой"]
|
||||
en = ["right click"]
|
||||
ua = ["правий клік"]
|
||||
|
||||
|
||||
[[commands]]
|
||||
id = "mouse_double_click"
|
||||
type = "lua"
|
||||
script = "dispatch.lua"
|
||||
sandbox = "full"
|
||||
timeout = 3000
|
||||
|
||||
[commands.phrases]
|
||||
ru = ["двойной клик", "дабл клик", "двойное нажатие"]
|
||||
en = ["double click"]
|
||||
ua = ["подвійний клік"]
|
||||
|
||||
|
||||
[[commands]]
|
||||
id = "mouse_scroll_down"
|
||||
type = "lua"
|
||||
script = "dispatch.lua"
|
||||
sandbox = "full"
|
||||
timeout = 3000
|
||||
|
||||
[commands.phrases]
|
||||
ru = ["промотай вниз", "прокрути вниз", "вниз промотай"]
|
||||
en = ["scroll down"]
|
||||
ua = ["прокрути вниз"]
|
||||
|
||||
|
||||
[[commands]]
|
||||
id = "mouse_scroll_up"
|
||||
type = "lua"
|
||||
script = "dispatch.lua"
|
||||
sandbox = "full"
|
||||
timeout = 3000
|
||||
|
||||
[commands.phrases]
|
||||
ru = ["промотай вверх", "прокрути вверх", "наверх промотай"]
|
||||
en = ["scroll up"]
|
||||
ua = ["прокрути вгору"]
|
||||
31
resources/commands/mouse/dispatch.lua
Normal file
31
resources/commands/mouse/dispatch.lua
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
local cmd_id = jarvis.context.command_id
|
||||
local helper = jarvis.context.command_path .. "\\_mouse_helper.ps1"
|
||||
|
||||
local actions = {
|
||||
mouse_left_click = "left",
|
||||
mouse_right_click = "right",
|
||||
mouse_middle_click = "middle",
|
||||
mouse_double_click = "double",
|
||||
mouse_scroll_up = "scroll_up",
|
||||
mouse_scroll_down = "scroll_down",
|
||||
}
|
||||
|
||||
local action = actions[cmd_id]
|
||||
if not action then
|
||||
jarvis.audio.play_error()
|
||||
return { chain = false }
|
||||
end
|
||||
|
||||
local res = jarvis.system.exec(string.format(
|
||||
'powershell -NoProfile -ExecutionPolicy Bypass -File "%s" -Action %s',
|
||||
helper, action
|
||||
))
|
||||
|
||||
if res.success then
|
||||
jarvis.audio.play_ok()
|
||||
else
|
||||
jarvis.log("error", "mouse " .. action .. " failed: " .. tostring(res.stderr))
|
||||
jarvis.audio.play_error()
|
||||
end
|
||||
|
||||
return { chain = false }
|
||||
23
resources/commands/random_choice/command.toml
Normal file
23
resources/commands/random_choice/command.toml
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
[[commands]]
|
||||
id = "random_choice"
|
||||
type = "lua"
|
||||
script = "pick.lua"
|
||||
sandbox = "full"
|
||||
timeout = 3000
|
||||
|
||||
[commands.phrases]
|
||||
ru = [
|
||||
"выбери из",
|
||||
"выбери",
|
||||
"решай за меня",
|
||||
"выбрать",
|
||||
]
|
||||
en = [
|
||||
"pick from",
|
||||
"choose from",
|
||||
"decide between",
|
||||
]
|
||||
ua = [
|
||||
"обери з",
|
||||
"вибери",
|
||||
]
|
||||
45
resources/commands/random_choice/pick.lua
Normal file
45
resources/commands/random_choice/pick.lua
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
local lang = jarvis.context.language
|
||||
local phrase = (jarvis.context.phrase or ""):lower()
|
||||
|
||||
local triggers = { "выбери из", "выбери", "решай за меня", "выбрать",
|
||||
"pick from", "choose from", "decide between",
|
||||
"обери з", "вибери" }
|
||||
local rest = phrase
|
||||
for _, t in ipairs(triggers) do
|
||||
local s, e = string.find(rest, t, 1, true)
|
||||
if s == 1 then
|
||||
rest = rest:sub(e + 1)
|
||||
break
|
||||
end
|
||||
end
|
||||
rest = rest:gsub("^%s+", ""):gsub("%s+$", "")
|
||||
|
||||
local separators = { " или ", " or ", ", ", " либо ", " чи " }
|
||||
local options = nil
|
||||
for _, sep in ipairs(separators) do
|
||||
local pattern = sep:gsub("([%-%.%+%[%]%(%)%$%^%%%?%*])", "%%%1")
|
||||
if rest:find(pattern) then
|
||||
options = {}
|
||||
for token in (rest .. sep):gmatch("(.-)" .. pattern) do
|
||||
token = token:gsub("^%s+", ""):gsub("%s+$", "")
|
||||
if token ~= "" then table.insert(options, token) end
|
||||
end
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if not options or #options < 2 then
|
||||
jarvis.system.notify("Choice", lang == "ru"
|
||||
and "Скажи: выбери из X или Y или Z"
|
||||
or "Say: pick from X or Y or Z")
|
||||
jarvis.audio.play_error()
|
||||
return { chain = false }
|
||||
end
|
||||
|
||||
math.randomseed(os.time() + math.floor(jarvis.context.time.second * 7919))
|
||||
local pick = options[math.random(1, #options)]
|
||||
|
||||
jarvis.system.notify(lang == "ru" and "Выбираю" or "Choice", pick)
|
||||
jarvis.speak((lang == "ru" and "Я выбираю " or "I pick ") .. pick, { lang = lang })
|
||||
jarvis.audio.play_ok()
|
||||
return { chain = false }
|
||||
Loading…
Add table
Add a link
Reference in a new issue