feat(macros): IMBA-7 voice-recorded command sequences (VoiceAttack-style)
Simpler than AHK keyboard hooks: record what the user SAID, replay each phrase
through the normal dispatch path. Works for any command jarvis already knows.
Flow:
"Запиши макрос работа" → start_recording("работа")
"Открой браузер" → buffered into the recording
"Запусти спотифай" → buffered
"Режим работа" → buffered
"Сохрани макрос" → save_recording() → persist
...later...
"Запусти макрос работа" → replay("работа") → fires each phrase in order
Core (crates/jarvis-core/src/macros.rs)
- Macro {name, steps, created_at, last_run} persisted at
<APP_CONFIG_DIR>/macros.json with atomic write-through.
- Recording state in-memory: RwLock<Option<RecordingState>>.
- start_recording / record_step / save_recording / cancel_recording.
- list / get / delete / mark_run.
- replay(name) spawns a thread that fires each step through a callback,
with 800ms inter-step delay. Caller returns immediately.
- is_macro_control filter prevents recording the meta-commands themselves
(no infinite recursion when "запиши макрос" runs during a recording).
- 3 unit tests (55 total).
Wiring (crates/jarvis-app/src/main.rs)
- macros::init() before listener starts.
- macros::set_replay_callback(move |phrase| text_cmd_tx.send(phrase)) — each
replay step gets queued as a synthetic text command, processed by the same
dispatcher the GUI uses. No special-case code path.
Recording hook (crates/jarvis-app/src/app.rs::execute_command)
- On successful command execution → call macros::record_step(text).
- Failed commands are NOT recorded (would fail on replay too).
- is_macro_control filter inside record_step skips meta-commands.
Lua API (crates/jarvis-core/src/lua/api/macros.rs)
- jarvis.macros.{start, save, cancel, replay, list, delete,
is_recording, recording_name}
Voice pack (resources/commands/macros/, 6 commands)
- "запиши макрос NAME" → macros.start_recording
- "сохрани макрос" → macros.save
- "отмени макрос" → macros.cancel
- "запусти макрос NAME" → macros.replay
- "какие у меня макросы" → macros.list
- "удали макрос NAME" → macros.delete
Build: cargo build --release -p jarvis-app green. 55/55 tests pass.
This commit is contained in:
parent
d63399f4c9
commit
dbb4a6b888
14 changed files with 594 additions and 2 deletions
4
resources/commands/macros/cancel.lua
Normal file
4
resources/commands/macros/cancel.lua
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
if jarvis.macros.cancel() then
|
||||
return jarvis.cmd.ok("Запись макроса отменена.")
|
||||
end
|
||||
return jarvis.cmd.not_found("Записи не было.")
|
||||
107
resources/commands/macros/command.toml
Normal file
107
resources/commands/macros/command.toml
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
# IMBA-7: Voice macros — record and replay sequences of voice commands.
|
||||
|
||||
[[commands]]
|
||||
id = "macros.start_recording"
|
||||
type = "lua"
|
||||
script = "start.lua"
|
||||
sandbox = "standard"
|
||||
timeout = 3000
|
||||
|
||||
[commands.phrases]
|
||||
ru = [
|
||||
"запиши макрос",
|
||||
"начни запись макроса",
|
||||
"записывай макрос",
|
||||
"новый макрос",
|
||||
]
|
||||
en = ["record macro", "start macro recording"]
|
||||
ua = ["запиши макрос"]
|
||||
|
||||
|
||||
[[commands]]
|
||||
id = "macros.save"
|
||||
type = "lua"
|
||||
script = "save.lua"
|
||||
sandbox = "standard"
|
||||
timeout = 3000
|
||||
|
||||
[commands.phrases]
|
||||
ru = [
|
||||
"сохрани макрос",
|
||||
"стоп макрос",
|
||||
"стоп запись макроса",
|
||||
"закончи запись",
|
||||
"макрос готов",
|
||||
]
|
||||
en = ["save macro", "stop macro", "finish macro"]
|
||||
ua = ["збережи макрос", "стоп макрос"]
|
||||
|
||||
|
||||
[[commands]]
|
||||
id = "macros.cancel"
|
||||
type = "lua"
|
||||
script = "cancel.lua"
|
||||
sandbox = "minimal"
|
||||
timeout = 2000
|
||||
|
||||
[commands.phrases]
|
||||
ru = [
|
||||
"отмени макрос",
|
||||
"отмени запись макроса",
|
||||
"отмени запись",
|
||||
"забудь макрос",
|
||||
]
|
||||
en = ["cancel macro", "discard macro"]
|
||||
ua = ["скасуй макрос"]
|
||||
|
||||
|
||||
[[commands]]
|
||||
id = "macros.replay"
|
||||
type = "lua"
|
||||
script = "replay.lua"
|
||||
sandbox = "standard"
|
||||
timeout = 60000
|
||||
|
||||
[commands.phrases]
|
||||
ru = [
|
||||
"запусти макрос",
|
||||
"выполни макрос",
|
||||
"воспроизведи макрос",
|
||||
"сыграй макрос",
|
||||
"проиграй макрос",
|
||||
]
|
||||
en = ["play macro", "replay macro", "run macro"]
|
||||
ua = ["запусти макрос"]
|
||||
|
||||
|
||||
[[commands]]
|
||||
id = "macros.list"
|
||||
type = "lua"
|
||||
script = "list.lua"
|
||||
sandbox = "minimal"
|
||||
timeout = 3000
|
||||
|
||||
[commands.phrases]
|
||||
ru = [
|
||||
"какие у меня макросы",
|
||||
"список макросов",
|
||||
"покажи макросы",
|
||||
]
|
||||
en = ["list macros", "show macros"]
|
||||
ua = ["список макросів"]
|
||||
|
||||
|
||||
[[commands]]
|
||||
id = "macros.delete"
|
||||
type = "lua"
|
||||
script = "delete.lua"
|
||||
sandbox = "minimal"
|
||||
timeout = 3000
|
||||
|
||||
[commands.phrases]
|
||||
ru = [
|
||||
"удали макрос",
|
||||
"забудь о макросе",
|
||||
]
|
||||
en = ["delete macro", "remove macro"]
|
||||
ua = ["видали макрос"]
|
||||
18
resources/commands/macros/delete.lua
Normal file
18
resources/commands/macros/delete.lua
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
local phrase = (jarvis.context.phrase or ""):lower()
|
||||
local name = jarvis.text.strip_trigger(phrase, {
|
||||
"удали макрос",
|
||||
"забудь о макросе",
|
||||
"delete macro",
|
||||
"remove macro",
|
||||
"видали макрос",
|
||||
})
|
||||
name = name:gsub("^[%s,:%.]+", ""):gsub("%s+$", "")
|
||||
|
||||
if name == "" then
|
||||
return jarvis.cmd.error("Какой макрос удалить?")
|
||||
end
|
||||
|
||||
if jarvis.macros.delete(name) then
|
||||
return jarvis.cmd.ok("Удалил макрос " .. name .. ".")
|
||||
end
|
||||
return jarvis.cmd.not_found("Макрос " .. name .. " не найден.")
|
||||
12
resources/commands/macros/list.lua
Normal file
12
resources/commands/macros/list.lua
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
local list = jarvis.macros.list()
|
||||
if #list == 0 then
|
||||
return jarvis.cmd.ok("Макросов нет.")
|
||||
end
|
||||
|
||||
local sample = math.min(#list, 5)
|
||||
local line = string.format("Макросов: %d. ", #list)
|
||||
for i = 1, sample do
|
||||
line = line .. list[i].name .. " (" .. list[i].steps_count .. " шагов)"
|
||||
if i < sample then line = line .. ", " end
|
||||
end
|
||||
return jarvis.cmd.ok(line .. ".")
|
||||
24
resources/commands/macros/replay.lua
Normal file
24
resources/commands/macros/replay.lua
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
-- "Запусти макрос работа"
|
||||
local phrase = (jarvis.context.phrase or ""):lower()
|
||||
local name = jarvis.text.strip_trigger(phrase, {
|
||||
"выполни макрос",
|
||||
"воспроизведи макрос",
|
||||
"запусти макрос",
|
||||
"сыграй макрос",
|
||||
"проиграй макрос",
|
||||
"play macro",
|
||||
"replay macro",
|
||||
"run macro",
|
||||
})
|
||||
name = name:gsub("^[%s,:%.]+", ""):gsub("%s+$", "")
|
||||
|
||||
if name == "" then
|
||||
return jarvis.cmd.error("Какой макрос запустить?")
|
||||
end
|
||||
|
||||
local ok, result = pcall(function() return jarvis.macros.replay(name) end)
|
||||
if not ok then
|
||||
return jarvis.cmd.not_found(tostring(result))
|
||||
end
|
||||
|
||||
return jarvis.cmd.ok(string.format("Запускаю %s, шагов: %d.", name, result))
|
||||
8
resources/commands/macros/save.lua
Normal file
8
resources/commands/macros/save.lua
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
local ok, result = pcall(function() return jarvis.macros.save() end)
|
||||
if not ok then
|
||||
-- result here is the error msg from save_recording
|
||||
return jarvis.cmd.not_found(tostring(result))
|
||||
end
|
||||
|
||||
local name = jarvis.macros.recording_name() or "макрос"
|
||||
return jarvis.cmd.ok(string.format("Сохранил %d шагов.", result))
|
||||
23
resources/commands/macros/start.lua
Normal file
23
resources/commands/macros/start.lua
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
-- "Запиши макрос работа"
|
||||
local phrase = (jarvis.context.phrase or ""):lower()
|
||||
local name = jarvis.text.strip_trigger(phrase, {
|
||||
"начни запись макроса",
|
||||
"записывай макрос",
|
||||
"запиши макрос",
|
||||
"новый макрос",
|
||||
"record macro",
|
||||
"start macro recording",
|
||||
})
|
||||
name = name:gsub("^[%s,:%.]+", ""):gsub("%s+$", "")
|
||||
|
||||
if name == "" then
|
||||
return jarvis.cmd.error("Как назвать макрос?")
|
||||
end
|
||||
|
||||
local ok, err = pcall(function() jarvis.macros.start(name) end)
|
||||
if not ok then
|
||||
jarvis.log("warn", "macros.start: " .. tostring(err))
|
||||
return jarvis.cmd.error("Не получилось начать запись.")
|
||||
end
|
||||
|
||||
return jarvis.cmd.ok(string.format("Записываю макрос %s. Говорите команды. Когда закончите — скажите сохрани макрос.", name))
|
||||
Loading…
Add table
Add a link
Reference in a new issue