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:
Bossiara13 2026-05-15 17:12:32 +03:00
parent d63399f4c9
commit dbb4a6b888
14 changed files with 594 additions and 2 deletions

View file

@ -464,6 +464,11 @@ fn execute_command(text: &str, rt: &tokio::runtime::Runtime) -> bool {
info!("Command executed successfully");
// voices::play_ok();
voices::play_random_from(cmd_config.get_sounds(&i18n::get_language()).as_slice());
// IMBA-7: if macro recording is active, capture this phrase.
// Skipped for macro-control commands themselves (filter inside record_step).
jarvis_core::macros::record_step(text);
ipc::send(IpcEvent::CommandExecuted {
id: cmd_config.id.clone(),
success: true,

View file

@ -88,6 +88,11 @@ fn main() -> Result<(), String> {
jarvis_core::scheduler::start_background();
}
eprintln!("[jarvis-app] step: macros::init");
if let Err(e) = jarvis_core::macros::init() {
warn!("Macros init failed: {}", e);
}
eprintln!("[jarvis-app] step: recorder::init");
if recorder::init().is_err() {
notify_mic_problem();
@ -153,9 +158,18 @@ fn main() -> Result<(), String> {
info!("Initializing IPC...");
ipc::init();
// channel for text commands (manually written in the GUI)
// channel for text commands (manually written in the GUI, OR fed by macro replay)
let (text_cmd_tx, text_cmd_rx) = mpsc::channel::<String>();
// Wire macro replay through the same text-command channel: each step of a
// replayed macro becomes a synthetic text command, processed in order.
let macro_tx = text_cmd_tx.clone();
jarvis_core::macros::set_replay_callback(Box::new(move |phrase: &str| {
if let Err(e) = macro_tx.send(phrase.to_string()) {
warn!("Macro replay: failed to enqueue '{}': {}", phrase, e);
}
}));
ipc::set_action_handler(move |action| {
match action {
IpcAction::Stop => {