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