Skip to main content

mocra_core/engine/task/
module.rs

1use crate::common::interface::{
2    DataMiddlewareHandle, DataStoreMiddlewareHandle, ModuleTrait, SyncBoxStream,
3};
4use crate::common::model::login_info::LoginInfo;
5use crate::common::model::message::TaskOutputEvent;
6use crate::common::model::scope::{AccountInfo, PlatformInfo};
7use crate::common::model::{Cookies, Headers, ModuleConfig, Request, Response};
8use crate::engine::task::module_dag_processor::ModuleDagProcessor;
9use crate::errors::RequestError;
10use crate::errors::Result;
11use futures::StreamExt;
12use log::warn;
13use serde::ser::SerializeStruct;
14use serde::{Serialize, Serializer};
15use serde_json::Map;
16use std::sync::Arc;
17use uuid::Uuid;
18
19/// Runtime module instance bound to account/platform context.
20///
21/// A Module aggregates static module behavior, resolved configuration,
22/// middleware bindings, and DAG-runtime metadata.
23#[derive(Clone)]
24pub struct Module {
25    /// Resolved module configuration.
26    pub config: Arc<ModuleConfig>,
27    /// Bound account info (lightweight, no sea-orm dependency).
28    pub account: AccountInfo,
29    /// Bound platform info (lightweight, no sea-orm dependency).
30    pub platform: PlatformInfo,
31    /// In-memory error counter snapshot.
32    pub error_times: u32,
33    /// Completion flag at module level.
34    pub finished: bool,
35    /// Data middleware names.
36    pub data_middleware: Vec<String>,
37    /// Download middleware names.
38    pub download_middleware: Vec<String>,
39    /// Module behavior implementation.
40    pub module: Arc<dyn ModuleTrait>,
41    /// Whether distributed locking is enabled.
42    pub locker: bool,
43    /// Lock TTL in seconds.
44    pub locker_ttl: u64,
45    /// Queue-backed DAG processor for node generation and parsing.
46    pub processor: ModuleDagProcessor,
47    /// Run identifier for cross-stage scoping.
48    pub run_id: Uuid,
49    /// Prefix request for fallback tracing.
50    pub prefix_request: Uuid,
51    /// Optional execution context for precise node targeting.
52    pub pending_ctx: Option<crate::common::model::ExecutionMark>,
53    /// Task metadata injected by TaskModuleProcessor.
54    pub bound_task_meta: Option<Map<String, serde_json::Value>>,
55    /// Login context injected by TaskModuleProcessor.
56    pub bound_login_info: Option<LoginInfo>,
57}
58impl Module {
59    /// Binds task metadata and optional login context.
60    pub fn bind_task_context(
61        &mut self,
62        task_meta: Map<String, serde_json::Value>,
63        login_info: Option<LoginInfo>,
64    ) {
65        self.bound_task_meta = Some(task_meta);
66        self.bound_login_info = login_info;
67    }
68
69    /// Returns task metadata and login info used by generate.
70    pub fn runtime_task_context(&self) -> (Map<String, serde_json::Value>, Option<LoginInfo>) {
71        (
72            self.bound_task_meta.clone().unwrap_or_default(),
73            self.bound_login_info.clone(),
74        )
75    }
76
77    /// Generates request stream for the current chain step.
78    ///
79    /// Delegates to `ModuleDagProcessor`, then enriches each request
80    /// with module/account/platform identity, middleware, config payloads, and run markers.
81    pub async fn generate(
82        &self,
83        task_meta: Map<String, serde_json::Value>,
84        login_info: Option<LoginInfo>,
85    ) -> Result<SyncBoxStream<'static, Request>> {
86        if self.module.should_login() && login_info.is_none() {
87            return Err(RequestError::NotLogin("module need login".into()).into());
88        }
89        let request_stream = self
90            .processor
91            .execute_generate(
92                self.config.clone(),
93                task_meta.clone(),
94                login_info.clone(),
95                self.pending_ctx.clone(),
96                Some(self.prefix_request),
97            )
98            .await?;
99
100        let module_name = self.module.name().clone();
101        let platform_name = self.platform.name.clone();
102        let download_middleware = self.download_middleware.clone();
103        let data_middleware = self.data_middleware.clone();
104        let account_name = self.account.name.clone();
105        let finished = self.finished;
106        let limit_id = self
107            .config
108            .get_config_value("limit_id")
109            .and_then(|v| v.as_str())
110            .map(|s| s.to_string());
111        let headers = self.module.headers().await;
112        let cookies = self.module.cookies().await;
113        let run_id = self.run_id;
114        let prefix_request = self.prefix_request;
115        let config = self.config.clone();
116        let stream = request_stream.map(move |mut request| {
117            if request.id.is_nil() {
118                request.id = Uuid::now_v7();
119            }
120            request.module = module_name.clone();
121            request.platform = platform_name.clone();
122            let mut merged_download_middleware = request.download_middleware.clone();
123            merged_download_middleware.extend(download_middleware.clone());
124            merged_download_middleware.sort();
125            merged_download_middleware.dedup();
126            request.download_middleware = merged_download_middleware;
127
128            let mut merged_data_middleware = request.data_middleware.clone();
129            merged_data_middleware.extend(data_middleware.clone());
130            merged_data_middleware.sort();
131            merged_data_middleware.dedup();
132            request.data_middleware = merged_data_middleware;
133            request.account = account_name.clone();
134            request.task_finished = finished;
135            request.run_id = run_id;
136            request.prefix_request = prefix_request;
137
138            if request.headers.is_empty() && !headers.is_empty() {
139                request = request.with_headers(headers.clone());
140            }
141            if !cookies.is_empty() {
142                request = request.with_cookies(cookies.clone());
143            }
144
145            if let Some(ref info) = login_info {
146                let cookies = Cookies::from(info);
147                let headers = Headers::from(info);
148                request.headers.merge(&headers);
149                request.cookies.merge(&cookies);
150                request = request.with_login_info(info);
151            }
152            request.limit_id = limit_id.clone().unwrap_or(request.module_id());
153            request = request
154                .with_module_config(&config)
155                .with_task_config(task_meta.clone());
156            if let Some(downloader) = config.get_config::<String>("downloader") {
157                request.downloader = downloader;
158            } else {
159                request.downloader = "request_downloader".to_string();
160            }
161            log::debug!(
162                "[Module] request prepared: account={} platform={} module={} url={} request_id={}",
163                request.account,
164                request.platform,
165                request.module,
166                request.url,
167                request.id
168            );
169            request
170        });
171        Ok(Box::pin(stream))
172    }
173
174    /// Builds the merged DAG definition and initializes the `ModuleDagProcessor`.
175    ///
176    /// Merges `dag_definition()` (custom graph) with `add_step()` (linear steps) when both
177    /// are provided, following `ModuleDagOrchestrator::compile_module` priority rules.
178    pub async fn add_step(&self) {
179        if let Err(e) = self.module.pre_process(Some(self.config.clone())).await {
180            warn!(
181                "module pre_process failed: account={} platform={} module={} error={}",
182                self.account.name,
183                self.platform.name,
184                self.module.name(),
185                e
186            );
187        }
188
189        use crate::engine::task::module_dag_orchestrator::ModuleDagOrchestrator;
190        let definition = ModuleDagOrchestrator
191            .build_definition(self.module.clone())
192            .await;
193        self.processor.init_from_definition(&definition).await;
194    }
195
196    /// Parses response at the routed DAG node and handles terminal lifecycle hook.
197    pub async fn parser(
198        &self,
199        response: Response,
200        config: Option<Arc<ModuleConfig>>,
201    ) -> Result<TaskOutputEvent> {
202        let cfg_for_post = config.clone();
203
204        let mut data = self.processor.execute_parse(response, config).await?;
205
206        // Enrich returned data with module/account/platform identifiers.
207        for d in data.data.iter_mut() {
208            d.module = self.module.name();
209            d.account = self.account.name.clone();
210            d.platform = self.platform.name.clone();
211        }
212
213        // Run post_process when at a leaf node with no pending next tasks.
214        let no_next_task = data.parser_task.is_empty();
215        if no_next_task {
216            self.module.post_process(cfg_for_post).await?;
217        }
218
219        // When the DAG signals an explicit stop, clean up the session using Module.run_id
220        // (the correctly-patched run_id from the task event).
221        // NOTE: ModuleDagProcessor.run_id may be stale when loaded from the factory cache
222        // since factory.load_parser_model / load_error_model update m.run_id but not
223        // m.processor.run_id. Using self.run_id here ensures the correct session key.
224        if data.stop.unwrap_or(false) {
225            self.processor.delete_session_for_run(self.run_id).await;
226        }
227
228        Ok(data)
229    }
230
231    /// Returns stable module runtime id in account-platform-module format.
232    pub fn id(&self) -> String {
233        format!(
234            "{}-{}-{}",
235            self.account.name,
236            self.platform.name,
237            self.module.name()
238        )
239    }
240}
241
242impl Serialize for Module {
243    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
244    where
245        S: Serializer,
246    {
247        let mut state = serializer.serialize_struct("Module", 8)?;
248        state.serialize_field("config", &self.config)?;
249        state.serialize_field("account", &self.account)?;
250        state.serialize_field("platform", &self.platform)?;
251        state.serialize_field("error_times", &self.error_times)?;
252        state.serialize_field("data_middleware", &self.data_middleware)?;
253        state.serialize_field("download_middleware", &self.download_middleware)?;
254        state.serialize_field("module", &self.module.name())?;
255        state.end()
256    }
257}
258
259/// Assembly helper for creating Module runtime instances.
260pub struct ModuleEntity {
261    pub module_work: Arc<dyn ModuleTrait>,
262    pub download_middleware: Vec<Arc<dyn ModuleTrait>>,
263    pub data_middleware: Vec<DataMiddlewareHandle>,
264    pub store_middleware: Vec<DataStoreMiddlewareHandle>,
265}
266
267impl From<Arc<dyn ModuleTrait>> for ModuleEntity {
268    fn from(module: Arc<dyn ModuleTrait>) -> Self {
269        ModuleEntity {
270            module_work: module,
271            download_middleware: vec![],
272            data_middleware: vec![],
273            store_middleware: vec![],
274        }
275    }
276}
277impl ModuleEntity {
278    /// Adds a download middleware module.
279    pub fn add_download_middleware(mut self, middleware: Arc<dyn ModuleTrait>) -> Self {
280        self.download_middleware.push(middleware);
281        self
282    }
283
284    /// Adds a data middleware implementation.
285    pub fn add_data_middleware(mut self, middleware: DataMiddlewareHandle) -> Self {
286        self.data_middleware.push(middleware);
287        self
288    }
289
290    /// Adds a data store middleware implementation.
291    pub fn add_store_middleware(mut self, middleware: DataStoreMiddlewareHandle) -> Self {
292        self.store_middleware.push(middleware);
293        self
294    }
295}
296
297#[cfg(test)]
298mod tests {
299    use super::*;
300    use crate::cacheable::CacheService;
301    use crate::common::interface::ModuleTrait;
302    use crate::engine::task::module_dag_processor::ModuleDagProcessor;
303    use async_trait::async_trait;
304
305    struct LoginRequiredTestModule;
306
307    #[async_trait]
308    impl ModuleTrait for LoginRequiredTestModule {
309        fn should_login(&self) -> bool {
310            true
311        }
312
313        fn name(&self) -> String {
314            "login_required_test".to_string()
315        }
316
317        fn version(&self) -> i32 {
318            1
319        }
320
321        fn default_arc() -> Arc<dyn ModuleTrait>
322        where
323            Self: Sized,
324        {
325            Arc::new(Self)
326        }
327    }
328
329    fn build_test_module(module_impl: Arc<dyn ModuleTrait>) -> Module {
330        Module {
331            config: Arc::new(ModuleConfig::default()),
332            account: AccountInfo {
333                id: 1,
334                name: "acc".to_string(),
335                config: serde_json::json!({}),
336            },
337            platform: PlatformInfo {
338                id: 1,
339                name: "pf".to_string(),
340                config: serde_json::json!({}),
341            },
342            error_times: 0,
343            finished: false,
344            data_middleware: vec![],
345            download_middleware: vec![],
346            module: module_impl,
347            locker: false,
348            locker_ttl: 0,
349            processor: ModuleDagProcessor::new(
350                "acc-pf-login_required_test".to_string(),
351                Arc::new(CacheService::new("test".to_string(), None, None)),
352                Uuid::now_v7(),
353                60,
354            ),
355            run_id: Uuid::now_v7(),
356            prefix_request: Uuid::nil(),
357            pending_ctx: None,
358            bound_task_meta: None,
359            bound_login_info: None,
360        }
361    }
362
363    #[test]
364    fn bind_task_context_roundtrip() {
365        let mut module = build_test_module(Arc::new(LoginRequiredTestModule));
366        let mut meta = Map::new();
367        meta.insert("k".to_string(), serde_json::json!("v"));
368        let login = LoginInfo::default();
369        module.bind_task_context(meta.clone(), Some(login.clone()));
370
371        let (bound_meta, bound_login) = module.runtime_task_context();
372        assert_eq!(bound_meta.get("k"), Some(&serde_json::json!("v")));
373        assert_eq!(
374            bound_login.as_ref().map(|x| x.useragent.clone()),
375            Some(login.useragent)
376        );
377    }
378
379    #[tokio::test]
380    async fn generate_returns_not_login_error_when_login_required_and_missing() {
381        let module = build_test_module(Arc::new(LoginRequiredTestModule));
382        match module.generate(Map::new(), None).await {
383            Ok(_) => panic!("should fail without login info"),
384            Err(err) => {
385                let msg = err.to_string();
386                assert!(
387                    msg.contains("module need login"),
388                    "unexpected error message: {msg}"
389                );
390            }
391        }
392    }
393}