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