feat: GUI /memory page + disk/date_math/sleep_timer packs (10 new commands)
GUI: Memory management completes the management trio (/macros + /scheduler + /memory).
Tauri commands (crates/jarvis-gui/src/tauri_commands/memory.rs)
- memory_list → Vec<MemoryFact{key, value, created_at, last_used_at, use_count}>
- memory_remember(k, v) → persist new fact (or overwrite)
- memory_forget(key) → delete by exact key
- memory_search(q, limit) → substring search
GUI /memory page (frontend/src/routes/memory/index.svelte)
- Top: add-fact form (key + value inputs + "+Добавить" button)
- Substring filter input for the list
- Per-card: key + use_count badge + value (highlighted box) + timestamps +
"Забыть" button with confirm() guard
- Auto-sorted by recency (last_used_at desc)
- Empty state shows voice-hint: 'скажи Jarvis-у "запомни что я люблю чай улун"'
Header (frontend/src/components/Header.svelte)
- New "Память" button between /scheduler and /settings.
- i18n: header-memory in ru/en/ua FTL files.
New voice packs
resources/commands/disk/ (2 cmds)
- disk.free "сколько свободно на диске" / "сколько места на диске C"
PowerShell Get-PSDrive → speaks "Свободно X ГБ из Y, это N%"
- disk.list "какие у меня диски" → "C 120 ГБ, D 300 ГБ, E 50 ГБ"
resources/commands/date_math/ (2 cmds)
- date.days_until "сколько дней до нового года" / "сколько до 8 марта" /
"сколько до 15 марта" — recognises Russian months,
holidays (Новый год, Рождество, 8 марта, 9 мая).
Auto-rolls to next year if target already passed.
Russian-grammar pluralisation (день/дня/дней).
- date.day_of_week "какой сегодня день недели" — Zeller's congruence,
maps to ru day name.
resources/commands/sleep_timer/ (3 cmds)
- sleep_timer.pause_in "выключи музыку через 30 минут"
→ scheduler one-shot lua action that fires media
VK_MEDIA_PLAY_PAUSE via the existing media_keys helper.
(Auto-generates _fire_pause.lua wrapper if missing.)
- sleep_timer.shutdown_in "выключи компьютер через 1 час"
→ shutdown.exe /s /t <secs>. Caps at 24 hours.
Speaks "Скажите 'отмени таймер' чтобы передумать."
- sleep_timer.cancel "отмени таймер выключения" / "не выключай компьютер"
→ shutdown /a + scheduler.remove("sleep_timer_pause"),
idempotent.
Pack count: 64 → 67. Tests: 112/112 pass.
Build: cargo build --release -p jarvis-gui green.
This commit is contained in:
parent
84d3b57ddc
commit
d3180b7d78
18 changed files with 717 additions and 4 deletions
|
|
@ -161,4 +161,5 @@ settings-profile = Profile
|
|||
|
||||
# Header buttons
|
||||
header-macros = Macros
|
||||
header-scheduler = Schedule
|
||||
header-scheduler = Schedule
|
||||
header-memory = Memory
|
||||
|
|
@ -161,4 +161,5 @@ settings-profile = Профиль
|
|||
|
||||
# Header buttons
|
||||
header-macros = Макросы
|
||||
header-scheduler = Расписание
|
||||
header-scheduler = Расписание
|
||||
header-memory = Память
|
||||
|
|
@ -161,4 +161,5 @@ settings-profile = Профіль
|
|||
|
||||
# Header buttons
|
||||
header-macros = Макроси
|
||||
header-scheduler = Розклад
|
||||
header-scheduler = Розклад
|
||||
header-memory = Пам'ять
|
||||
|
|
@ -122,6 +122,12 @@ fn main() {
|
|||
tauri_commands::scheduler_list,
|
||||
tauri_commands::scheduler_remove,
|
||||
tauri_commands::scheduler_clear,
|
||||
|
||||
// Long-term memory facts
|
||||
tauri_commands::memory_list,
|
||||
tauri_commands::memory_remember,
|
||||
tauri_commands::memory_forget,
|
||||
tauri_commands::memory_search,
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
|
|
|
|||
|
|
@ -49,4 +49,8 @@ pub use macros::*;
|
|||
|
||||
// Scheduler tasks
|
||||
mod scheduler;
|
||||
pub use scheduler::*;
|
||||
pub use scheduler::*;
|
||||
|
||||
// Long-term memory facts
|
||||
mod memory;
|
||||
pub use memory::*;
|
||||
45
crates/jarvis-gui/src/tauri_commands/memory.rs
Normal file
45
crates/jarvis-gui/src/tauri_commands/memory.rs
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
//! Tauri commands for the long-term memory store.
|
||||
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct MemoryFact {
|
||||
pub key: String,
|
||||
pub value: String,
|
||||
pub created_at: i64,
|
||||
pub last_used_at: i64,
|
||||
pub use_count: u64,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn memory_list() -> Vec<MemoryFact> {
|
||||
jarvis_core::long_term_memory::all().into_iter().map(|r| MemoryFact {
|
||||
key: r.key,
|
||||
value: r.value,
|
||||
created_at: r.created_at,
|
||||
last_used_at: r.last_used_at,
|
||||
use_count: r.use_count,
|
||||
}).collect()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn memory_remember(key: String, value: String) -> Result<(), String> {
|
||||
jarvis_core::long_term_memory::remember(&key, &value)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn memory_forget(key: String) -> bool {
|
||||
jarvis_core::long_term_memory::forget(&key)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn memory_search(query: String, limit: Option<usize>) -> Vec<MemoryFact> {
|
||||
jarvis_core::long_term_memory::search(&query, limit.unwrap_or(10))
|
||||
.into_iter().map(|r| MemoryFact {
|
||||
key: r.key,
|
||||
value: r.value,
|
||||
created_at: r.created_at,
|
||||
last_used_at: r.last_used_at,
|
||||
use_count: r.use_count,
|
||||
}).collect()
|
||||
}
|
||||
|
|
@ -80,6 +80,10 @@
|
|||
<span class="btn-text">{t('header-scheduler') || 'Расписание'}</span>
|
||||
</button>
|
||||
|
||||
<button class="header-btn" on:click={() => $goto('/memory')} title="Long-term memory">
|
||||
<span class="btn-text">{t('header-memory') || 'Память'}</span>
|
||||
</button>
|
||||
|
||||
<button class="header-btn" on:click={() => $goto('/settings')}>
|
||||
<span class="btn-text">{t('header-settings')}</span>
|
||||
</button>
|
||||
|
|
|
|||
249
frontend/src/routes/memory/index.svelte
Normal file
249
frontend/src/routes/memory/index.svelte
Normal file
|
|
@ -0,0 +1,249 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from "svelte"
|
||||
import { invoke } from "@tauri-apps/api/core"
|
||||
import { goto } from "@roxi/routify"
|
||||
|
||||
import HDivider from "@/components/elements/HDivider.svelte"
|
||||
import Footer from "@/components/Footer.svelte"
|
||||
import {
|
||||
Button, Space, Text, Notification, Badge, TextInput, Loader, Textarea,
|
||||
} from "@svelteuidev/core"
|
||||
import { Plus, TrashIcon, CrossCircled, MagnifyingGlass } from "radix-icons-svelte"
|
||||
|
||||
interface MemoryFact {
|
||||
key: string
|
||||
value: string
|
||||
created_at: number
|
||||
last_used_at: number
|
||||
use_count: number
|
||||
}
|
||||
|
||||
let facts: MemoryFact[] = []
|
||||
let loading = true
|
||||
let error = ""
|
||||
let filter = ""
|
||||
|
||||
// new-fact form
|
||||
let newKey = ""
|
||||
let newValue = ""
|
||||
|
||||
async function load() {
|
||||
loading = true
|
||||
error = ""
|
||||
try {
|
||||
facts = await invoke<MemoryFact[]>("memory_list")
|
||||
facts = [...facts].sort((a, b) => b.last_used_at - a.last_used_at)
|
||||
} catch (e) {
|
||||
error = String(e)
|
||||
} finally {
|
||||
loading = false
|
||||
}
|
||||
}
|
||||
|
||||
async function addFact() {
|
||||
const k = newKey.trim()
|
||||
const v = newValue.trim()
|
||||
if (!k) { error = "Ключ не может быть пустым"; return }
|
||||
if (!v) { error = "Значение не может быть пустым"; return }
|
||||
try {
|
||||
await invoke("memory_remember", { key: k, value: v })
|
||||
newKey = ""
|
||||
newValue = ""
|
||||
await load()
|
||||
} catch (e) { error = String(e) }
|
||||
}
|
||||
|
||||
async function deleteFact(key: string) {
|
||||
if (!confirm(`Забыть факт "${key}"?`)) return
|
||||
try {
|
||||
await invoke("memory_forget", { key })
|
||||
await load()
|
||||
} catch (e) { error = String(e) }
|
||||
}
|
||||
|
||||
onMount(load)
|
||||
|
||||
$: visible = filter
|
||||
? facts.filter(f =>
|
||||
f.key.toLowerCase().includes(filter.toLowerCase()) ||
|
||||
f.value.toLowerCase().includes(filter.toLowerCase())
|
||||
)
|
||||
: facts
|
||||
|
||||
function fmtDate(ts: number): string {
|
||||
if (!ts) return "—"
|
||||
return new Date(ts * 1000).toLocaleString()
|
||||
}
|
||||
</script>
|
||||
|
||||
<Space h="xl" />
|
||||
|
||||
<h2 class="page-title">Память</h2>
|
||||
<Text size="sm" color="gray">
|
||||
Долговременные факты о пользователе. LLM получает их в системном промпте автоматически,
|
||||
когда фразой пересекаются. Хранятся в <code>long_term_memory.json</code>.
|
||||
</Text>
|
||||
|
||||
<Space h="md" />
|
||||
|
||||
<div class="new-fact">
|
||||
<div class="fact-row">
|
||||
<TextInput placeholder="Ключ (напр. 'любимый чай')" variant="filled" bind:value={newKey} />
|
||||
<TextInput placeholder="Значение (напр. 'улун')" variant="filled" bind:value={newValue} />
|
||||
<Button color="lime" radius="md" size="sm" on:click={addFact}>
|
||||
<Plus size={14} /> Добавить
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Space h="md" />
|
||||
|
||||
<TextInput
|
||||
placeholder="Фильтр по ключу или значению"
|
||||
variant="filled"
|
||||
icon={MagnifyingGlass}
|
||||
bind:value={filter}
|
||||
/>
|
||||
|
||||
<Space h="sm" />
|
||||
|
||||
{#if error}
|
||||
<Notification
|
||||
title="Ошибка"
|
||||
icon={CrossCircled}
|
||||
color="red"
|
||||
on:close={() => { error = "" }}
|
||||
>
|
||||
{error}
|
||||
</Notification>
|
||||
<Space h="sm" />
|
||||
{/if}
|
||||
|
||||
{#if loading}
|
||||
<Loader />
|
||||
{:else if facts.length === 0}
|
||||
<Text size="sm" color="gray">
|
||||
Память пустая. Добавь факт сверху или скажи Jarvis-у "запомни что я люблю чай улун".
|
||||
</Text>
|
||||
{:else if visible.length === 0}
|
||||
<Text size="sm" color="gray">
|
||||
По фильтру ничего не найдено. Всего фактов: {facts.length}.
|
||||
</Text>
|
||||
{:else}
|
||||
<div class="fact-list">
|
||||
{#each visible as fact}
|
||||
<div class="fact-card">
|
||||
<div class="fact-header">
|
||||
<span class="fact-key">{fact.key}</span>
|
||||
<div class="fact-meta-inline">
|
||||
<Badge color="cyan" variant="outline" size="sm">
|
||||
{fact.use_count}× использований
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
<div class="fact-value">
|
||||
{fact.value}
|
||||
</div>
|
||||
<div class="fact-meta">
|
||||
<small>Создан: {fmtDate(fact.created_at)}</small>
|
||||
<small>Последний доступ: {fmtDate(fact.last_used_at)}</small>
|
||||
</div>
|
||||
<div class="fact-actions">
|
||||
<Button
|
||||
color="red" radius="md" size="xs" uppercase
|
||||
on:click={() => deleteFact(fact.key)}
|
||||
>
|
||||
<TrashIcon size={14} /> Забыть
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<Space h="xl" />
|
||||
|
||||
<Button color="gray" radius="md" size="sm" uppercase fullSize on:click={() => $goto("/")}>
|
||||
Назад
|
||||
</Button>
|
||||
|
||||
<HDivider />
|
||||
<Footer />
|
||||
|
||||
<style lang="scss">
|
||||
.page-title {
|
||||
margin: 0 0 4px 0;
|
||||
color: #fff;
|
||||
font-size: 1.3rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.new-fact {
|
||||
background: rgba(30, 40, 45, 0.55);
|
||||
border: 1px solid rgba(255, 255, 255, 0.07);
|
||||
border-radius: 10px;
|
||||
padding: 0.75rem;
|
||||
|
||||
.fact-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 2fr auto;
|
||||
gap: 0.5rem;
|
||||
align-items: end;
|
||||
}
|
||||
}
|
||||
|
||||
.fact-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.fact-card {
|
||||
background: rgba(30, 40, 45, 0.75);
|
||||
border: 1px solid rgba(82, 254, 254, 0.15);
|
||||
border-radius: 10px;
|
||||
padding: 0.85rem 1rem;
|
||||
}
|
||||
|
||||
.fact-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 0.4rem;
|
||||
}
|
||||
|
||||
.fact-key {
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.fact-meta-inline {
|
||||
display: flex;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.fact-value {
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border-radius: 6px;
|
||||
padding: 0.5rem 0.7rem;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.45em;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.fact-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.75rem;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
font-size: 0.7rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.fact-actions {
|
||||
display: flex;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
</style>
|
||||
37
resources/commands/date_math/command.toml
Normal file
37
resources/commands/date_math/command.toml
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
# Date arithmetic — "сколько дней до нового года" / "сколько до зарплаты 15-го"
|
||||
|
||||
[[commands]]
|
||||
id = "date.days_until"
|
||||
type = "lua"
|
||||
script = "days_until.lua"
|
||||
sandbox = "minimal"
|
||||
timeout = 2000
|
||||
|
||||
[commands.phrases]
|
||||
ru = [
|
||||
"сколько дней до",
|
||||
"сколько до нового года",
|
||||
"сколько до",
|
||||
"до нового года",
|
||||
"когда",
|
||||
]
|
||||
en = ["how many days until", "days until"]
|
||||
ua = ["скільки днів до"]
|
||||
|
||||
|
||||
[[commands]]
|
||||
id = "date.day_of_week"
|
||||
type = "lua"
|
||||
script = "day_of_week.lua"
|
||||
sandbox = "minimal"
|
||||
timeout = 2000
|
||||
|
||||
[commands.phrases]
|
||||
ru = [
|
||||
"какой сегодня день недели",
|
||||
"какой день недели",
|
||||
"какой сейчас день",
|
||||
"сегодня какой",
|
||||
]
|
||||
en = ["what day is today", "day of week"]
|
||||
ua = ["який сьогодні день тижня"]
|
||||
21
resources/commands/date_math/day_of_week.lua
Normal file
21
resources/commands/date_math/day_of_week.lua
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
local t = jarvis.context.time
|
||||
|
||||
-- Compute day-of-week using Zeller's congruence (0=Saturday).
|
||||
local y, m, d = t.year, t.month, t.day
|
||||
if m < 3 then m = m + 12; y = y - 1 end
|
||||
local K = y % 100
|
||||
local J = math.floor(y / 100)
|
||||
local h = (d + math.floor(13 * (m + 1) / 5) + K + math.floor(K / 4) + math.floor(J / 4) - 2 * J) % 7
|
||||
-- h: 0=Saturday, 1=Sunday, 2=Monday, ... 6=Friday
|
||||
|
||||
local days_ru = {
|
||||
[0] = "суббота",
|
||||
[1] = "воскресенье",
|
||||
[2] = "понедельник",
|
||||
[3] = "вторник",
|
||||
[4] = "среда",
|
||||
[5] = "четверг",
|
||||
[6] = "пятница",
|
||||
}
|
||||
|
||||
return jarvis.cmd.ok("Сегодня " .. (days_ru[h] or "неизвестный день") .. ".")
|
||||
74
resources/commands/date_math/days_until.lua
Normal file
74
resources/commands/date_math/days_until.lua
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
-- "Сколько дней до нового года" / "сколько до 15 марта"
|
||||
local phrase = (jarvis.context.phrase or ""):lower()
|
||||
local t = jarvis.context.time
|
||||
local current_year = t.year
|
||||
|
||||
local MONTHS = {
|
||||
["январ"]=1, ["феврал"]=2, ["март"]=3, ["апрел"]=4, ["май"]=5, ["мая"]=5, ["июн"]=6,
|
||||
["июл"]=7, ["август"]=8, ["сентябр"]=9, ["октябр"]=10, ["ноябр"]=11, ["декабр"]=12,
|
||||
}
|
||||
|
||||
-- Special phrases
|
||||
local target_y, target_m, target_d
|
||||
|
||||
if phrase:find("нов") and phrase:find("год") then
|
||||
target_y, target_m, target_d = current_year + 1, 1, 1
|
||||
if t.month == 1 and t.day == 1 then target_y = current_year end
|
||||
elseif phrase:find("рожд") then
|
||||
target_y, target_m, target_d = current_year, 1, 7 -- ru orthodox
|
||||
elseif phrase:find("8 март") or phrase:find("восьмого март") then
|
||||
target_y, target_m, target_d = current_year, 3, 8
|
||||
elseif phrase:find("9 ма") or phrase:find("девятое ма") then
|
||||
target_y, target_m, target_d = current_year, 5, 9
|
||||
end
|
||||
|
||||
-- Generic "до N <месяц>"
|
||||
if not target_y then
|
||||
local d = tonumber(phrase:match("до%s+(%d+)"))
|
||||
if d then
|
||||
for stem, month in pairs(MONTHS) do
|
||||
if phrase:find(stem) then
|
||||
target_d, target_m, target_y = d, month, current_year
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if not target_y then
|
||||
return jarvis.cmd.error("Не понял дату.")
|
||||
end
|
||||
|
||||
-- If target this year already passed, jump to next year
|
||||
local function days_since_epoch(y, m, d)
|
||||
-- approximate: works for diffs within ~100 years
|
||||
local total = y * 365 + math.floor(y / 4) - math.floor(y / 100) + math.floor(y / 400)
|
||||
local cum = { 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334 }
|
||||
total = total + cum[m] + (d - 1)
|
||||
-- leap-day adjustment
|
||||
local leap = (y % 4 == 0 and y % 100 ~= 0) or (y % 400 == 0)
|
||||
if leap and m > 2 then total = total + 1 end
|
||||
return total
|
||||
end
|
||||
|
||||
local now = days_since_epoch(t.year, t.month, t.day)
|
||||
local target = days_since_epoch(target_y, target_m, target_d)
|
||||
|
||||
if target < now then
|
||||
target_y = target_y + 1
|
||||
target = days_since_epoch(target_y, target_m, target_d)
|
||||
end
|
||||
|
||||
local diff = target - now
|
||||
if diff == 0 then
|
||||
return jarvis.cmd.ok("Сегодня.")
|
||||
end
|
||||
|
||||
-- Russian grammar: 1 день, 2-4 дня, 5+ дней
|
||||
local last = diff % 10
|
||||
local unit = "дней"
|
||||
if last == 1 and diff % 100 ~= 11 then unit = "день"
|
||||
elseif last >= 2 and last <= 4 and (diff % 100 < 10 or diff % 100 >= 20) then unit = "дня"
|
||||
end
|
||||
|
||||
return jarvis.cmd.ok(string.format("%d %s.", diff, unit))
|
||||
36
resources/commands/disk/command.toml
Normal file
36
resources/commands/disk/command.toml
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
# Disk space — free GB on system drive, or named drive.
|
||||
|
||||
[[commands]]
|
||||
id = "disk.free"
|
||||
type = "lua"
|
||||
script = "free.lua"
|
||||
sandbox = "full"
|
||||
timeout = 5000
|
||||
|
||||
[commands.phrases]
|
||||
ru = [
|
||||
"сколько свободного места",
|
||||
"сколько места на диске",
|
||||
"сколько свободно на диске",
|
||||
"место на диске",
|
||||
"свободное место",
|
||||
]
|
||||
en = ["disk space", "free space", "how much disk"]
|
||||
ua = ["скільки місця на диску"]
|
||||
|
||||
|
||||
[[commands]]
|
||||
id = "disk.list"
|
||||
type = "lua"
|
||||
script = "list.lua"
|
||||
sandbox = "full"
|
||||
timeout = 5000
|
||||
|
||||
[commands.phrases]
|
||||
ru = [
|
||||
"какие у меня диски",
|
||||
"список дисков",
|
||||
"перечень дисков",
|
||||
]
|
||||
en = ["list drives", "what drives do I have"]
|
||||
ua = ["які диски"]
|
||||
35
resources/commands/disk/free.lua
Normal file
35
resources/commands/disk/free.lua
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
-- "Сколько свободно на диске C" / "сколько места"
|
||||
local phrase = (jarvis.context.phrase or ""):upper()
|
||||
local letter = phrase:match("([A-Z])%s*ДИСК") or phrase:match("ДИСК%s*([A-Z])") or
|
||||
phrase:match("DRIVE%s*([A-Z])") or "C"
|
||||
|
||||
local ps = string.format(
|
||||
"$d = Get-PSDrive %s -ErrorAction SilentlyContinue; " ..
|
||||
"if ($d) { '{0}|{1}' -f " ..
|
||||
"[math]::Round($d.Free/1GB,1), " ..
|
||||
"[math]::Round(($d.Free + $d.Used)/1GB,0) } else { 'NONE' }",
|
||||
letter
|
||||
)
|
||||
local res = jarvis.system.exec(string.format(
|
||||
'powershell -NoProfile -Command "%s"', ps:gsub('"', '\\"')
|
||||
))
|
||||
|
||||
if not res.success then
|
||||
return jarvis.cmd.error("Не получилось.")
|
||||
end
|
||||
|
||||
local out = (res.stdout or ""):gsub("%s+", "")
|
||||
if out == "NONE" or out == "" then
|
||||
return jarvis.cmd.not_found("Диск " .. letter .. " не найден.")
|
||||
end
|
||||
|
||||
local free_gb, total_gb = out:match("([%d%.]+)|(%d+)")
|
||||
if not free_gb then
|
||||
return jarvis.cmd.error("Не понял ответ системы.")
|
||||
end
|
||||
|
||||
local pct = math.floor(tonumber(free_gb) / tonumber(total_gb) * 100 + 0.5)
|
||||
return jarvis.cmd.ok(string.format(
|
||||
"На диске %s свободно %s гигабайт из %s, это %d процентов.",
|
||||
letter, free_gb, total_gb, pct
|
||||
))
|
||||
30
resources/commands/disk/list.lua
Normal file
30
resources/commands/disk/list.lua
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
local ps =
|
||||
"Get-PSDrive -PSProvider FileSystem | " ..
|
||||
"Where-Object { $_.Used -ne $null -or $_.Free -ne $null } | " ..
|
||||
"ForEach-Object { '{0}:{1}' -f $_.Name, [math]::Round($_.Free/1GB,0) } | " ..
|
||||
"ForEach-Object { Write-Host -NoNewline ($_+' ') }"
|
||||
|
||||
local res = jarvis.system.exec(string.format(
|
||||
'powershell -NoProfile -Command "%s"', ps:gsub('"', '\\"')
|
||||
))
|
||||
|
||||
if not res.success then
|
||||
return jarvis.cmd.error("Не получилось.")
|
||||
end
|
||||
|
||||
local out = (res.stdout or ""):gsub("^%s+", ""):gsub("%s+$", "")
|
||||
if out == "" then
|
||||
return jarvis.cmd.not_found("Дисков не нашёл.")
|
||||
end
|
||||
|
||||
-- "C:120 D:300 E:50" → "C 120 ГБ, D 300 ГБ, ..."
|
||||
local parts = {}
|
||||
for letter, gb in out:gmatch("([A-Z]):(%d+)") do
|
||||
table.insert(parts, letter .. " " .. gb .. " гигабайт")
|
||||
end
|
||||
|
||||
if #parts == 0 then
|
||||
return jarvis.cmd.error("Не понял ответ системы.")
|
||||
end
|
||||
|
||||
return jarvis.cmd.ok("Свободно: " .. table.concat(parts, ", ") .. ".")
|
||||
9
resources/commands/sleep_timer/cancel.lua
Normal file
9
resources/commands/sleep_timer/cancel.lua
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
-- Cancel both shutdown and pause timers (best-effort, ok if one was inactive).
|
||||
local removed_pause = jarvis.scheduler.remove("sleep_timer_pause")
|
||||
local sd_res = jarvis.system.exec("shutdown.exe /a")
|
||||
local sd_ok = sd_res and sd_res.success
|
||||
|
||||
if removed_pause or sd_ok then
|
||||
return jarvis.cmd.ok("Таймер отменён.")
|
||||
end
|
||||
return jarvis.cmd.not_found("Активных таймеров нет.")
|
||||
56
resources/commands/sleep_timer/command.toml
Normal file
56
resources/commands/sleep_timer/command.toml
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
# Sleep timer — pause media + optional shutdown after N minutes.
|
||||
|
||||
[[commands]]
|
||||
id = "sleep_timer.pause_in"
|
||||
type = "lua"
|
||||
script = "pause_in.lua"
|
||||
sandbox = "standard"
|
||||
timeout = 5000
|
||||
|
||||
[commands.phrases]
|
||||
ru = [
|
||||
"выключи музыку через",
|
||||
"приостанови музыку через",
|
||||
"пауза через",
|
||||
"пауза музыки через",
|
||||
]
|
||||
en = ["pause music in", "pause in"]
|
||||
ua = ["пауза через"]
|
||||
|
||||
|
||||
[[commands]]
|
||||
id = "sleep_timer.shutdown_in"
|
||||
type = "lua"
|
||||
script = "shutdown_in.lua"
|
||||
sandbox = "full"
|
||||
timeout = 5000
|
||||
|
||||
[commands.phrases]
|
||||
ru = [
|
||||
"выключи компьютер через",
|
||||
"выключи пк через",
|
||||
"выключи комп через",
|
||||
"таймер выключения",
|
||||
"ложусь спать через",
|
||||
]
|
||||
en = ["shutdown in", "sleep timer"]
|
||||
ua = ["вимкни комп'ютер через"]
|
||||
|
||||
|
||||
[[commands]]
|
||||
id = "sleep_timer.cancel"
|
||||
type = "lua"
|
||||
script = "cancel.lua"
|
||||
sandbox = "standard"
|
||||
timeout = 3000
|
||||
|
||||
[commands.phrases]
|
||||
ru = [
|
||||
"отмени таймер выключения",
|
||||
"отмени таймер сна",
|
||||
"отмени паузу",
|
||||
"не выключай компьютер",
|
||||
"не выключай пк",
|
||||
]
|
||||
en = ["cancel sleep timer", "cancel shutdown"]
|
||||
ua = ["скасуй таймер вимкнення"]
|
||||
58
resources/commands/sleep_timer/pause_in.lua
Normal file
58
resources/commands/sleep_timer/pause_in.lua
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
-- "Выключи музыку через 30 минут" → scheduler one-shot фраза-команда "пауза"
|
||||
local phrase = (jarvis.context.phrase or ""):lower()
|
||||
local n_str, unit = phrase:match("(%d+)%s+(%S+)")
|
||||
if not n_str then
|
||||
return jarvis.cmd.error("Не понял через сколько.")
|
||||
end
|
||||
local n = tonumber(n_str)
|
||||
local secs
|
||||
if unit:match("^минут") then secs = "in " .. n .. " minutes"
|
||||
elseif unit:match("^час") then secs = "in " .. n .. " hours"
|
||||
elseif unit:match("^секунд") then secs = "in " .. n .. " seconds"
|
||||
else
|
||||
return jarvis.cmd.error("Не понял единицу.")
|
||||
end
|
||||
|
||||
-- Build path to media_keys/_media.ps1 (lives next door)
|
||||
local cmd_path = jarvis.context.command_path
|
||||
local helper = cmd_path:gsub("sleep_timer$", "media_keys") .. "\\_media.ps1"
|
||||
helper = helper:gsub("/", "\\")
|
||||
|
||||
-- VK_MEDIA_PLAY_PAUSE = 0xB3 = 179
|
||||
local lua_path = cmd_path:gsub("\\", "/") .. "/_pause_lua.lua"
|
||||
-- Simpler: just speak "пауза" and let agent router handle it? No — agent
|
||||
-- router only fires for unmatched commands. Better: directly invoke media key
|
||||
-- via a one-shot Lua script.
|
||||
|
||||
local script_path = cmd_path .. "\\_fire_pause.lua"
|
||||
script_path = script_path:gsub("/", "\\")
|
||||
|
||||
-- Write the helper script once
|
||||
local exists = jarvis.fs.is_file(script_path)
|
||||
if not exists then
|
||||
jarvis.fs.write(script_path, string.format([[
|
||||
local helper = "%s"
|
||||
jarvis.system.exec(string.format(
|
||||
'powershell -NoProfile -ExecutionPolicy Bypass -File "%%s" 179',
|
||||
helper
|
||||
))
|
||||
return { chain = false }
|
||||
]], helper:gsub("\\", "\\\\")))
|
||||
end
|
||||
|
||||
local ok, err = pcall(function()
|
||||
jarvis.scheduler.remove("sleep_timer_pause")
|
||||
jarvis.scheduler.add({
|
||||
id = "sleep_timer_pause",
|
||||
name = "Sleep timer: pause music",
|
||||
schedule = secs,
|
||||
action = { type = "lua", script_path = script_path },
|
||||
})
|
||||
end)
|
||||
|
||||
if not ok then
|
||||
jarvis.log("warn", "sleep_timer.pause_in: " .. tostring(err))
|
||||
return jarvis.cmd.error("Не получилось.")
|
||||
end
|
||||
|
||||
return jarvis.cmd.ok("Поставлю на паузу через " .. n_str .. " " .. unit .. ".")
|
||||
46
resources/commands/sleep_timer/shutdown_in.lua
Normal file
46
resources/commands/sleep_timer/shutdown_in.lua
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
-- "Выключи компьютер через 30 минут" → shutdown.exe /s /t <seconds>
|
||||
-- Использует штатный shutdown с обратным таймером.
|
||||
local phrase = (jarvis.context.phrase or ""):lower()
|
||||
local n_str, unit = phrase:match("(%d+)%s+(%S+)")
|
||||
if not n_str then
|
||||
return jarvis.cmd.error("Не понял через сколько.")
|
||||
end
|
||||
local n = tonumber(n_str)
|
||||
local secs
|
||||
if unit:match("^минут") then secs = n * 60
|
||||
elseif unit:match("^час") then secs = n * 3600
|
||||
elseif unit:match("^секунд") then secs = n
|
||||
else
|
||||
return jarvis.cmd.error("Не понял единицу. Минуты или часы.")
|
||||
end
|
||||
|
||||
if secs > 86400 then
|
||||
return jarvis.cmd.error("Слишком большое значение — максимум 24 часа.")
|
||||
end
|
||||
|
||||
local cmd = string.format(
|
||||
'shutdown.exe /s /t %d /c "Sleep timer fired"',
|
||||
secs
|
||||
)
|
||||
local res = jarvis.system.exec(cmd)
|
||||
if not res.success then
|
||||
return jarvis.cmd.error("Не получилось поставить таймер.")
|
||||
end
|
||||
|
||||
-- Pluralisation
|
||||
local label
|
||||
if unit:match("^минут") then
|
||||
local last = n % 10
|
||||
if last == 1 and n % 100 ~= 11 then label = n .. " минуту"
|
||||
elseif last >= 2 and last <= 4 and (n % 100 < 10 or n % 100 >= 20) then label = n .. " минуты"
|
||||
else label = n .. " минут" end
|
||||
elseif unit:match("^час") then
|
||||
local last = n % 10
|
||||
if last == 1 and n % 100 ~= 11 then label = n .. " час"
|
||||
elseif last >= 2 and last <= 4 and (n % 100 < 10 or n % 100 >= 20) then label = n .. " часа"
|
||||
else label = n .. " часов" end
|
||||
else
|
||||
label = n .. " секунд"
|
||||
end
|
||||
|
||||
return jarvis.cmd.ok("Выключу компьютер через " .. label .. ". Скажите 'отмени таймер выключения' чтобы передумать.")
|
||||
Loading…
Add table
Add a link
Reference in a new issue