Skip to main content

maa_framework/
notification.rs

1//! Structured notification parsing for event callbacks.
2//!
3//! This module provides typed structures and parsing utilities for the event
4//! notifications from MaaFramework. Instead of manually parsing JSON strings,
5//! use these helpers to work with strongly-typed event data.
6//!
7//! # Example
8//!
9//! ```
10//! use maa_framework::notification::{self, NotificationType};
11//! use maa_framework::tasker::Tasker;
12//!
13//! fn example(tasker: &Tasker) -> maa_framework::error::MaaResult<()> {
14//!     tasker.add_sink(|message, details| {
15//!         let noti_type = notification::parse_type(message);
16//!         
17//!         if message.starts_with("Resource.Loading") {
18//!             if let Some(detail) = notification::parse_resource_loading(details) {
19//!                 println!("Resource {} loading: {:?}", detail.res_id, noti_type);
20//!             }
21//!         }
22//!     })?;
23//!     Ok(())
24//! }
25//! ```
26
27use serde::{Deserialize, Serialize};
28use serde_json::Value;
29
30// === Notification Type ===
31
32/// Type of notification event.
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
34pub enum NotificationType {
35    /// Operation is starting
36    Starting,
37    /// Operation succeeded
38    Succeeded,
39    /// Operation failed
40    Failed,
41    /// Unknown notification type
42    Unknown,
43}
44
45impl NotificationType {
46    /// Check if this is a starting notification.
47    pub fn is_starting(&self) -> bool {
48        matches!(self, Self::Starting)
49    }
50
51    /// Check if this is a succeeded notification.
52    pub fn is_succeeded(&self) -> bool {
53        matches!(self, Self::Succeeded)
54    }
55
56    /// Check if this is a failed notification.
57    pub fn is_failed(&self) -> bool {
58        matches!(self, Self::Failed)
59    }
60
61    /// Check if the operation is complete (succeeded or failed).
62    pub fn is_complete(&self) -> bool {
63        matches!(self, Self::Succeeded | Self::Failed)
64    }
65}
66
67impl From<&str> for NotificationType {
68    fn from(s: &str) -> Self {
69        if s.ends_with(".Starting") {
70            NotificationType::Starting
71        } else if s.ends_with(".Succeeded") {
72            NotificationType::Succeeded
73        } else if s.ends_with(".Failed") {
74            NotificationType::Failed
75        } else {
76            NotificationType::Unknown
77        }
78    }
79}
80
81// === Event Detail Structures ===
82
83/// Resource loading event detail.
84#[derive(Debug, Clone, Serialize, Deserialize)]
85pub struct ResourceLoadingDetail {
86    /// Resource ID
87    pub res_id: i64,
88    /// Resource hash
89    pub hash: String,
90    /// Path being loaded
91    pub path: String,
92}
93
94/// Controller action event detail.
95#[derive(Debug, Clone, Serialize, Deserialize)]
96pub struct ControllerActionDetail {
97    /// Controller ID
98    pub ctrl_id: i64,
99    /// Device UUID
100    pub uuid: String,
101    /// Action name
102    pub action: String,
103    /// Action parameters
104    #[serde(default)]
105    pub param: Value,
106}
107
108/// Tasker task event detail.
109#[derive(Debug, Clone, Serialize, Deserialize)]
110pub struct TaskerTaskDetail {
111    /// Task ID
112    pub task_id: i64,
113    /// Entry node name
114    pub entry: String,
115    /// Device UUID
116    #[serde(default)]
117    pub uuid: String,
118    /// Resource hash
119    #[serde(default)]
120    pub hash: String,
121}
122
123/// Next list item for node traversal.
124#[derive(Debug, Clone, Serialize, Deserialize)]
125pub struct NextListItem {
126    /// Node name
127    pub name: String,
128    /// Whether to jump back after execution
129    #[serde(default)]
130    pub jump_back: bool,
131    /// Whether this is an anchor node
132    #[serde(default)]
133    pub anchor: bool,
134}
135
136/// Node next list event detail.
137#[derive(Debug, Clone, Serialize, Deserialize)]
138pub struct NodeNextListDetail {
139    /// Task ID
140    pub task_id: i64,
141    /// Current node name
142    pub name: String,
143    /// List of next nodes
144    #[serde(default)]
145    pub list: Vec<NextListItem>,
146    /// Focus configuration
147    #[serde(default)]
148    pub focus: Value,
149}
150
151/// Node recognition event detail.
152#[derive(Debug, Clone, Serialize, Deserialize)]
153pub struct NodeRecognitionDetail {
154    /// Task ID
155    pub task_id: i64,
156    /// Recognition ID
157    pub reco_id: i64,
158    /// Node name
159    pub name: String,
160    /// Focus configuration
161    #[serde(default)]
162    pub focus: Value,
163}
164
165/// Node action event detail.
166#[derive(Debug, Clone, Serialize, Deserialize)]
167pub struct NodeActionDetail {
168    /// Task ID
169    pub task_id: i64,
170    /// Action ID
171    pub action_id: i64,
172    /// Node name
173    pub name: String,
174    /// Focus configuration
175    #[serde(default)]
176    pub focus: Value,
177}
178
179/// Node pipeline node event detail.
180#[derive(Debug, Clone, Serialize, Deserialize)]
181pub struct NodePipelineNodeDetail {
182    /// Task ID
183    pub task_id: i64,
184    /// Node ID
185    pub node_id: i64,
186    /// Node name
187    pub name: String,
188    /// Focus configuration
189    #[serde(default)]
190    pub focus: Value,
191}
192
193// === Message Constants ===
194
195/// Notification message constants.
196pub mod msg {
197    // Resource events
198    pub const RESOURCE_LOADING_STARTING: &str = "Resource.Loading.Starting";
199    pub const RESOURCE_LOADING_SUCCEEDED: &str = "Resource.Loading.Succeeded";
200    pub const RESOURCE_LOADING_FAILED: &str = "Resource.Loading.Failed";
201
202    // Controller events
203    pub const CONTROLLER_ACTION_STARTING: &str = "Controller.Action.Starting";
204    pub const CONTROLLER_ACTION_SUCCEEDED: &str = "Controller.Action.Succeeded";
205    pub const CONTROLLER_ACTION_FAILED: &str = "Controller.Action.Failed";
206
207    // Tasker events
208    pub const TASKER_TASK_STARTING: &str = "Tasker.Task.Starting";
209    pub const TASKER_TASK_SUCCEEDED: &str = "Tasker.Task.Succeeded";
210    pub const TASKER_TASK_FAILED: &str = "Tasker.Task.Failed";
211
212    // Node pipeline events
213    pub const NODE_PIPELINE_NODE_STARTING: &str = "Node.PipelineNode.Starting";
214    pub const NODE_PIPELINE_NODE_SUCCEEDED: &str = "Node.PipelineNode.Succeeded";
215    pub const NODE_PIPELINE_NODE_FAILED: &str = "Node.PipelineNode.Failed";
216
217    // Node recognition events
218    pub const NODE_RECOGNITION_STARTING: &str = "Node.Recognition.Starting";
219    pub const NODE_RECOGNITION_SUCCEEDED: &str = "Node.Recognition.Succeeded";
220    pub const NODE_RECOGNITION_FAILED: &str = "Node.Recognition.Failed";
221
222    // Node action events
223    pub const NODE_ACTION_STARTING: &str = "Node.Action.Starting";
224    pub const NODE_ACTION_SUCCEEDED: &str = "Node.Action.Succeeded";
225    pub const NODE_ACTION_FAILED: &str = "Node.Action.Failed";
226
227    // Node next list events
228    pub const NODE_NEXT_LIST_STARTING: &str = "Node.NextList.Starting";
229    pub const NODE_NEXT_LIST_SUCCEEDED: &str = "Node.NextList.Succeeded";
230    pub const NODE_NEXT_LIST_FAILED: &str = "Node.NextList.Failed";
231
232    // Node recognition node trace events
233    pub const NODE_RECOGNITION_NODE_STARTING: &str = "Node.RecognitionNode.Starting";
234    pub const NODE_RECOGNITION_NODE_SUCCEEDED: &str = "Node.RecognitionNode.Succeeded";
235    pub const NODE_RECOGNITION_NODE_FAILED: &str = "Node.RecognitionNode.Failed";
236
237    // Node action node trace events
238    pub const NODE_ACTION_NODE_STARTING: &str = "Node.ActionNode.Starting";
239    pub const NODE_ACTION_NODE_SUCCEEDED: &str = "Node.ActionNode.Succeeded";
240    pub const NODE_ACTION_NODE_FAILED: &str = "Node.ActionNode.Failed";
241}
242
243// === Parse Functions ===
244
245/// Parse notification type from message string.
246///
247/// # Example
248/// ```
249/// use maa_framework::notification::{self, NotificationType};
250///
251/// let noti_type = notification::parse_type("Resource.Loading.Succeeded");
252/// assert_eq!(noti_type, NotificationType::Succeeded);
253/// ```
254pub fn parse_type(msg: &str) -> NotificationType {
255    NotificationType::from(msg)
256}
257
258/// Parse resource loading event detail from JSON.
259pub fn parse_resource_loading(details: &str) -> Option<ResourceLoadingDetail> {
260    serde_json::from_str(details).ok()
261}
262
263/// Parse controller action event detail from JSON.
264pub fn parse_controller_action(details: &str) -> Option<ControllerActionDetail> {
265    serde_json::from_str(details).ok()
266}
267
268/// Parse tasker task event detail from JSON.
269pub fn parse_tasker_task(details: &str) -> Option<TaskerTaskDetail> {
270    serde_json::from_str(details).ok()
271}
272
273/// Parse node next list event detail from JSON.
274pub fn parse_node_next_list(details: &str) -> Option<NodeNextListDetail> {
275    serde_json::from_str(details).ok()
276}
277
278/// Parse node recognition event detail from JSON.
279pub fn parse_node_recognition(details: &str) -> Option<NodeRecognitionDetail> {
280    serde_json::from_str(details).ok()
281}
282
283/// Parse node action event detail from JSON.
284pub fn parse_node_action(details: &str) -> Option<NodeActionDetail> {
285    serde_json::from_str(details).ok()
286}
287
288/// Parse node pipeline node event detail from JSON.
289pub fn parse_node_pipeline_node(details: &str) -> Option<NodePipelineNodeDetail> {
290    serde_json::from_str(details).ok()
291}
292
293// === Context Event ===
294
295/// Enum representing parsed Context events.
296#[derive(Debug, Clone)]
297pub enum ContextEvent {
298    NodeNextList(NotificationType, NodeNextListDetail),
299    NodeRecognition(NotificationType, NodeRecognitionDetail),
300    NodeAction(NotificationType, NodeActionDetail),
301    NodePipelineNode(NotificationType, NodePipelineNodeDetail),
302    NodeRecognitionNode(NotificationType, NodePipelineNodeDetail),
303    NodeActionNode(NotificationType, NodePipelineNodeDetail),
304    Unknown(String, Value),
305}
306
307impl ContextEvent {
308    /// Parse a raw notification into a strongly-typed ContextEvent.
309    pub fn from_notification(msg: &str, details: &str) -> Option<Self> {
310        let noti_type = NotificationType::from(msg);
311
312        let parse_json = || -> Option<Value> { serde_json::from_str(details).ok() };
313
314        if msg.starts_with("Node.NextList") {
315            let detail = parse_node_next_list(details)?;
316            return Some(ContextEvent::NodeNextList(noti_type, detail));
317        }
318
319        if msg.starts_with("Node.Recognition.") {
320            let detail = parse_node_recognition(details)?;
321            return Some(ContextEvent::NodeRecognition(noti_type, detail));
322        }
323
324        if msg.starts_with("Node.Action.") {
325            let detail = parse_node_action(details)?;
326            return Some(ContextEvent::NodeAction(noti_type, detail));
327        }
328
329        if msg.starts_with("Node.PipelineNode") {
330            let detail = parse_node_pipeline_node(details)?;
331            return Some(ContextEvent::NodePipelineNode(noti_type, detail));
332        }
333
334        if msg.starts_with("Node.RecognitionNode") {
335            let detail = parse_node_pipeline_node(details)?;
336            return Some(ContextEvent::NodeRecognitionNode(noti_type, detail));
337        }
338
339        if msg.starts_with("Node.ActionNode") {
340            let detail = parse_node_pipeline_node(details)?;
341            return Some(ContextEvent::NodeActionNode(noti_type, detail));
342        }
343
344        Some(ContextEvent::Unknown(
345            msg.to_string(),
346            parse_json().unwrap_or(Value::Null),
347        ))
348    }
349}
350
351// === Event Sink Traits ===
352
353/// Trait for handling resource events.
354pub trait ResourceEventHandler: Send + Sync {
355    /// Called when a resource loading event occurs.
356    fn on_resource_loading(&self, _noti_type: NotificationType, _detail: ResourceLoadingDetail) {}
357
358    /// Called when an unknown notification is received.
359    fn on_unknown(&self, _msg: &str, _details: &Value) {}
360}
361
362/// Trait for handling controller events.
363pub trait ControllerEventHandler: Send + Sync {
364    /// Called when a controller action event occurs.
365    fn on_controller_action(&self, _noti_type: NotificationType, _detail: ControllerActionDetail) {}
366
367    /// Called when an unknown notification is received.
368    fn on_unknown(&self, _msg: &str, _details: &Value) {}
369}
370
371/// Trait for handling tasker events.
372pub trait TaskerEventHandler: Send + Sync {
373    /// Called when a tasker task event occurs.
374    fn on_tasker_task(&self, _noti_type: NotificationType, _detail: TaskerTaskDetail) {}
375
376    /// Called when an unknown notification is received.
377    fn on_unknown(&self, _msg: &str, _details: &Value) {}
378}
379
380/// Trait for handling context/node events.
381pub trait ContextEventHandler: Send + Sync {
382    /// Called when a node next list event occurs.
383    fn on_node_next_list(&self, _noti_type: NotificationType, _detail: NodeNextListDetail) {}
384
385    /// Called when a node recognition event occurs.
386    fn on_node_recognition(&self, _noti_type: NotificationType, _detail: NodeRecognitionDetail) {}
387
388    /// Called when a node action event occurs.
389    fn on_node_action(&self, _noti_type: NotificationType, _detail: NodeActionDetail) {}
390
391    /// Called when a node pipeline node event occurs.
392    fn on_node_pipeline_node(&self, _noti_type: NotificationType, _detail: NodePipelineNodeDetail) {
393    }
394
395    /// Called when a node recognition node (trace) event occurs.
396    fn on_node_recognition_node(
397        &self,
398        _noti_type: NotificationType,
399        _detail: NodePipelineNodeDetail,
400    ) {
401    }
402
403    /// Called when a node action node (trace) event occurs.
404    fn on_node_action_node(&self, _noti_type: NotificationType, _detail: NodePipelineNodeDetail) {}
405
406    /// Called when an unknown notification is received.
407    fn on_unknown(&self, _msg: &str, _details: &Value) {}
408}
409
410// === MaaEvent ===
411
412/// Unified event enum for all framework notifications.
413///
414/// This enum encapsulates all possible event types emitted by the framework, providing
415/// a type-safe way to handle notifications from `Tasker`, `Controller`, or `Resource`.
416/// Each variant wraps a detailed structure containing relevant information about the event.
417///
418/// # See Also
419/// * [`EventSink`](crate::event_sink::EventSink) - The trait for receiving these events.
420/// * [`Tasker::add_event_sink`](crate::tasker::Tasker::add_event_sink) - Registering an event sink.
421#[derive(Debug, Clone)]
422pub enum MaaEvent {
423    // --- Resource Events ---
424    /// Triggered when a resource starts loading.
425    ResourceLoadingStarting(ResourceLoadingDetail),
426    /// Triggered when a resource is successfully loaded.
427    ResourceLoadingSucceeded(ResourceLoadingDetail),
428    /// Triggered when resource loading fails.
429    ResourceLoadingFailed(ResourceLoadingDetail),
430
431    // --- Controller Events ---
432    /// Triggered before a controller performs an action (e.g., click, swipe).
433    ControllerActionStarting(ControllerActionDetail),
434    /// Triggered after a controller action completes successfully.
435    ControllerActionSucceeded(ControllerActionDetail),
436    /// Triggered if a controller action fails.
437    ControllerActionFailed(ControllerActionDetail),
438
439    // --- Tasker Events ---
440    /// Triggered when a task begins execution.
441    TaskerTaskStarting(TaskerTaskDetail),
442    /// Triggered when a task completes successfully.
443    TaskerTaskSucceeded(TaskerTaskDetail),
444    /// Triggered when a task fails.
445    TaskerTaskFailed(TaskerTaskDetail),
446
447    // --- Node Pipeline Events ---
448    /// Triggered when a node in the pipeline starts execution.
449    NodePipelineNodeStarting(NodePipelineNodeDetail),
450    /// Triggered when a node in the pipeline completes successfully.
451    NodePipelineNodeSucceeded(NodePipelineNodeDetail),
452    /// Triggered when a node in the pipeline fails.
453    NodePipelineNodeFailed(NodePipelineNodeDetail),
454
455    // --- Node Recognition Events ---
456    /// Triggered when image recognition begins for a node.
457    NodeRecognitionStarting(NodeRecognitionDetail),
458    /// Triggered when image recognition succeeds.
459    NodeRecognitionSucceeded(NodeRecognitionDetail),
460    /// Triggered when image recognition fails.
461    NodeRecognitionFailed(NodeRecognitionDetail),
462
463    // --- Node Action Events ---
464    /// Triggered before a node action is executed.
465    NodeActionStarting(NodeActionDetail),
466    /// Triggered after a node action completes successfully.
467    NodeActionSucceeded(NodeActionDetail),
468    /// Triggered if a node action fails.
469    NodeActionFailed(NodeActionDetail),
470
471    // --- Node Next List Events ---
472    /// Triggered when processing the "next" list for a node.
473    NodeNextListStarting(NodeNextListDetail),
474    /// Triggered when the "next" list processing succeeds.
475    NodeNextListSucceeded(NodeNextListDetail),
476    /// Triggered when the "next" list processing fails.
477    NodeNextListFailed(NodeNextListDetail),
478
479    // --- Trace Events ---
480    /// Trace event: Recognition node starting.
481    NodeRecognitionNodeStarting(NodePipelineNodeDetail),
482    /// Trace event: Recognition node succeeded.
483    NodeRecognitionNodeSucceeded(NodePipelineNodeDetail),
484    /// Trace event: Recognition node failed.
485    NodeRecognitionNodeFailed(NodePipelineNodeDetail),
486
487    /// Trace event: Action node starting.
488    NodeActionNodeStarting(NodePipelineNodeDetail),
489    /// Trace event: Action node succeeded.
490    NodeActionNodeSucceeded(NodePipelineNodeDetail),
491    /// Trace event: Action node failed.
492    NodeActionNodeFailed(NodePipelineNodeDetail),
493
494    // --- Fallback ---
495    /// Represents an unknown or unparsable event.
496    ///
497    /// This variant is used as a fallback when the event message is not recognized
498    /// or if JSON deserialization fails.
499    Unknown {
500        /// The raw message string.
501        msg: String,
502        /// The raw JSON detail string.
503        raw_json: String,
504        err: Option<String>,
505    },
506}
507
508impl MaaEvent {
509    /// Parses a notification message and detail string into a `MaaEvent`.
510    ///
511    /// This function handles the conversion from the raw C-string values provided by the
512    /// framework callback into safe, typed Rust structures.
513    ///
514    /// # Arguments
515    /// * `msg` - The notification type identifier (e.g., "Resource.Loading.Starting").
516    /// * `details` - The JSON string containing event details.
517    pub fn from_json(msg: &str, details: &str) -> Self {
518        match msg {
519            // Resource
520            msg::RESOURCE_LOADING_STARTING => match serde_json::from_str(details) {
521                Ok(d) => MaaEvent::ResourceLoadingStarting(d),
522                Err(e) => MaaEvent::Unknown {
523                    msg: msg.to_string(),
524                    raw_json: details.to_string(),
525                    err: Some(e.to_string()),
526                },
527            },
528            msg::RESOURCE_LOADING_SUCCEEDED => match serde_json::from_str(details) {
529                Ok(d) => MaaEvent::ResourceLoadingSucceeded(d),
530                Err(e) => MaaEvent::Unknown {
531                    msg: msg.to_string(),
532                    raw_json: details.to_string(),
533                    err: Some(e.to_string()),
534                },
535            },
536            msg::RESOURCE_LOADING_FAILED => match serde_json::from_str(details) {
537                Ok(d) => MaaEvent::ResourceLoadingFailed(d),
538                Err(e) => MaaEvent::Unknown {
539                    msg: msg.to_string(),
540                    raw_json: details.to_string(),
541                    err: Some(e.to_string()),
542                },
543            },
544
545            // Controller
546            msg::CONTROLLER_ACTION_STARTING => match serde_json::from_str(details) {
547                Ok(d) => MaaEvent::ControllerActionStarting(d),
548                Err(e) => MaaEvent::Unknown {
549                    msg: msg.to_string(),
550                    raw_json: details.to_string(),
551                    err: Some(e.to_string()),
552                },
553            },
554            msg::CONTROLLER_ACTION_SUCCEEDED => match serde_json::from_str(details) {
555                Ok(d) => MaaEvent::ControllerActionSucceeded(d),
556                Err(e) => MaaEvent::Unknown {
557                    msg: msg.to_string(),
558                    raw_json: details.to_string(),
559                    err: Some(e.to_string()),
560                },
561            },
562            msg::CONTROLLER_ACTION_FAILED => match serde_json::from_str(details) {
563                Ok(d) => MaaEvent::ControllerActionFailed(d),
564                Err(e) => MaaEvent::Unknown {
565                    msg: msg.to_string(),
566                    raw_json: details.to_string(),
567                    err: Some(e.to_string()),
568                },
569            },
570
571            // Tasker
572            msg::TASKER_TASK_STARTING => match serde_json::from_str(details) {
573                Ok(d) => MaaEvent::TaskerTaskStarting(d),
574                Err(e) => MaaEvent::Unknown {
575                    msg: msg.to_string(),
576                    raw_json: details.to_string(),
577                    err: Some(e.to_string()),
578                },
579            },
580            msg::TASKER_TASK_SUCCEEDED => match serde_json::from_str(details) {
581                Ok(d) => MaaEvent::TaskerTaskSucceeded(d),
582                Err(e) => MaaEvent::Unknown {
583                    msg: msg.to_string(),
584                    raw_json: details.to_string(),
585                    err: Some(e.to_string()),
586                },
587            },
588            msg::TASKER_TASK_FAILED => match serde_json::from_str(details) {
589                Ok(d) => MaaEvent::TaskerTaskFailed(d),
590                Err(e) => MaaEvent::Unknown {
591                    msg: msg.to_string(),
592                    raw_json: details.to_string(),
593                    err: Some(e.to_string()),
594                },
595            },
596
597            // Node Pipeline
598            msg::NODE_PIPELINE_NODE_STARTING => match serde_json::from_str(details) {
599                Ok(d) => MaaEvent::NodePipelineNodeStarting(d),
600                Err(e) => MaaEvent::Unknown {
601                    msg: msg.to_string(),
602                    raw_json: details.to_string(),
603                    err: Some(e.to_string()),
604                },
605            },
606            msg::NODE_PIPELINE_NODE_SUCCEEDED => match serde_json::from_str(details) {
607                Ok(d) => MaaEvent::NodePipelineNodeSucceeded(d),
608                Err(e) => MaaEvent::Unknown {
609                    msg: msg.to_string(),
610                    raw_json: details.to_string(),
611                    err: Some(e.to_string()),
612                },
613            },
614            msg::NODE_PIPELINE_NODE_FAILED => match serde_json::from_str(details) {
615                Ok(d) => MaaEvent::NodePipelineNodeFailed(d),
616                Err(e) => MaaEvent::Unknown {
617                    msg: msg.to_string(),
618                    raw_json: details.to_string(),
619                    err: Some(e.to_string()),
620                },
621            },
622
623            // Node Recognition
624            msg::NODE_RECOGNITION_STARTING => match serde_json::from_str(details) {
625                Ok(d) => MaaEvent::NodeRecognitionStarting(d),
626                Err(e) => MaaEvent::Unknown {
627                    msg: msg.to_string(),
628                    raw_json: details.to_string(),
629                    err: Some(e.to_string()),
630                },
631            },
632            msg::NODE_RECOGNITION_SUCCEEDED => match serde_json::from_str(details) {
633                Ok(d) => MaaEvent::NodeRecognitionSucceeded(d),
634                Err(e) => MaaEvent::Unknown {
635                    msg: msg.to_string(),
636                    raw_json: details.to_string(),
637                    err: Some(e.to_string()),
638                },
639            },
640            msg::NODE_RECOGNITION_FAILED => match serde_json::from_str(details) {
641                Ok(d) => MaaEvent::NodeRecognitionFailed(d),
642                Err(e) => MaaEvent::Unknown {
643                    msg: msg.to_string(),
644                    raw_json: details.to_string(),
645                    err: Some(e.to_string()),
646                },
647            },
648
649            // Node Action
650            msg::NODE_ACTION_STARTING => match serde_json::from_str(details) {
651                Ok(d) => MaaEvent::NodeActionStarting(d),
652                Err(e) => MaaEvent::Unknown {
653                    msg: msg.to_string(),
654                    raw_json: details.to_string(),
655                    err: Some(e.to_string()),
656                },
657            },
658            msg::NODE_ACTION_SUCCEEDED => match serde_json::from_str(details) {
659                Ok(d) => MaaEvent::NodeActionSucceeded(d),
660                Err(e) => MaaEvent::Unknown {
661                    msg: msg.to_string(),
662                    raw_json: details.to_string(),
663                    err: Some(e.to_string()),
664                },
665            },
666            msg::NODE_ACTION_FAILED => match serde_json::from_str(details) {
667                Ok(d) => MaaEvent::NodeActionFailed(d),
668                Err(e) => MaaEvent::Unknown {
669                    msg: msg.to_string(),
670                    raw_json: details.to_string(),
671                    err: Some(e.to_string()),
672                },
673            },
674
675            // Node Next List
676            msg::NODE_NEXT_LIST_STARTING => match serde_json::from_str(details) {
677                Ok(d) => MaaEvent::NodeNextListStarting(d),
678                Err(e) => MaaEvent::Unknown {
679                    msg: msg.to_string(),
680                    raw_json: details.to_string(),
681                    err: Some(e.to_string()),
682                },
683            },
684            msg::NODE_NEXT_LIST_SUCCEEDED => match serde_json::from_str(details) {
685                Ok(d) => MaaEvent::NodeNextListSucceeded(d),
686                Err(e) => MaaEvent::Unknown {
687                    msg: msg.to_string(),
688                    raw_json: details.to_string(),
689                    err: Some(e.to_string()),
690                },
691            },
692            msg::NODE_NEXT_LIST_FAILED => match serde_json::from_str(details) {
693                Ok(d) => MaaEvent::NodeNextListFailed(d),
694                Err(e) => MaaEvent::Unknown {
695                    msg: msg.to_string(),
696                    raw_json: details.to_string(),
697                    err: Some(e.to_string()),
698                },
699            },
700
701            // Node Recognition Node
702            msg::NODE_RECOGNITION_NODE_STARTING => match serde_json::from_str(details) {
703                Ok(d) => MaaEvent::NodeRecognitionNodeStarting(d),
704                Err(e) => MaaEvent::Unknown {
705                    msg: msg.to_string(),
706                    raw_json: details.to_string(),
707                    err: Some(e.to_string()),
708                },
709            },
710            msg::NODE_RECOGNITION_NODE_SUCCEEDED => match serde_json::from_str(details) {
711                Ok(d) => MaaEvent::NodeRecognitionNodeSucceeded(d),
712                Err(e) => MaaEvent::Unknown {
713                    msg: msg.to_string(),
714                    raw_json: details.to_string(),
715                    err: Some(e.to_string()),
716                },
717            },
718            msg::NODE_RECOGNITION_NODE_FAILED => match serde_json::from_str(details) {
719                Ok(d) => MaaEvent::NodeRecognitionNodeFailed(d),
720                Err(e) => MaaEvent::Unknown {
721                    msg: msg.to_string(),
722                    raw_json: details.to_string(),
723                    err: Some(e.to_string()),
724                },
725            },
726
727            // Node Action Node
728            msg::NODE_ACTION_NODE_STARTING => match serde_json::from_str(details) {
729                Ok(d) => MaaEvent::NodeActionNodeStarting(d),
730                Err(e) => MaaEvent::Unknown {
731                    msg: msg.to_string(),
732                    raw_json: details.to_string(),
733                    err: Some(e.to_string()),
734                },
735            },
736            msg::NODE_ACTION_NODE_SUCCEEDED => match serde_json::from_str(details) {
737                Ok(d) => MaaEvent::NodeActionNodeSucceeded(d),
738                Err(e) => MaaEvent::Unknown {
739                    msg: msg.to_string(),
740                    raw_json: details.to_string(),
741                    err: Some(e.to_string()),
742                },
743            },
744            msg::NODE_ACTION_NODE_FAILED => match serde_json::from_str(details) {
745                Ok(d) => MaaEvent::NodeActionNodeFailed(d),
746                Err(e) => MaaEvent::Unknown {
747                    msg: msg.to_string(),
748                    raw_json: details.to_string(),
749                    err: Some(e.to_string()),
750                },
751            },
752
753            _ => MaaEvent::Unknown {
754                msg: msg.to_string(),
755                raw_json: details.to_string(),
756                err: None,
757            },
758        }
759    }
760}
761
762// === Tests ===
763
764#[cfg(test)]
765mod tests {
766    use super::*;
767    use serde_json::json;
768
769    #[test]
770    fn test_context_event_parsing_logic() {
771        let msg_reco = "Node.Recognition.Succeeded";
772        let detail_reco =
773            json!({ "task_id": 1, "reco_id": 100, "name": "R", "focus": null }).to_string();
774
775        if let Some(ContextEvent::NodeRecognition(t, _)) =
776            ContextEvent::from_notification(msg_reco, &detail_reco)
777        {
778            assert_eq!(t, NotificationType::Succeeded);
779        } else {
780            panic!("Node.Recognition parse failed");
781        }
782
783        let msg_node = "Node.RecognitionNode.Starting";
784        let detail_node =
785            json!({ "task_id": 1, "node_id": 200, "name": "N", "focus": null }).to_string();
786
787        if let Some(ContextEvent::NodeRecognitionNode(t, _)) =
788            ContextEvent::from_notification(msg_node, &detail_node)
789        {
790            assert_eq!(t, NotificationType::Starting);
791        } else {
792            panic!("Node.RecognitionNode parse failed");
793        }
794    }
795}