//! Node.js 流水线命令:脚本由后端 `/api/v1/nodejs-scripts` 下发, //! 由打包在 `resources/nodejs/node.exe` 的 Node 解释器执行。 //! //! 前端用法示例: //! //! ```ignore //! import { invoke } from "@tauri-apps/api/core"; //! import { listen } from "@tauri-apps/api/event"; //! //! const off = await listen("nodejs:event", (e) => console.log(e.payload)); //! const result = await invoke("run_nodejs_script", { //! scriptName: "douyin_pipeline.js", //! params: { shareText: "...", modelConfig: {...} }, //! }); //! off(); //! ``` //! //! 与 QuickJS 流水线对比: //! - 入口约定:`globalThis.__nodejsMain(params)`(QuickJS 那边是 `__quickjsMain`) //! - 事件通道:`nodejs:event`(QuickJS 那边是 `quickjs:event`),payload 形状一致 //! - __native 只暴露 `emitProgress` / `log` 两个能力,其它 IO / 网络 //! 直接用 Node 自带 API 即可,无需 Rust 端再封一遍桥。 use serde::Serialize; use serde_json::Value; use tauri::{AppHandle, Emitter, Window}; use tokio::sync::mpsc; use crate::app_config::AppConfig; use crate::js_runtime::PipelineEvent; use crate::nodejs; const EVENT_NAME: &str = "nodejs:event"; #[derive(Debug, Serialize, Clone)] #[serde(tag = "type", rename_all = "camelCase")] pub enum FrontendEvent { Progress { #[serde(rename = "scriptName")] script_name: String, status: String, }, Log { #[serde(rename = "scriptName")] script_name: String, level: String, msg: String, #[serde(skip_serializing_if = "Option::is_none")] fields: Option, }, } fn log_node_event(script_name: &str, level: &str, msg: &str, fields: &Option) { let extra = fields .as_ref() .map(|f| format!(" {f}")) .unwrap_or_default(); let line = format!("[{script_name}] {msg}{extra}"); match level { "error" => log::error!(target: "nodejs", "{line}"), "warn" => log::warn!(target: "nodejs", "{line}"), _ => log::info!(target: "nodejs", "{line}"), } } fn spawn_event_pump( app: AppHandle, window: Window, script_name: String, mut rx: mpsc::UnboundedReceiver, ) -> tokio::task::JoinHandle<()> { let win_label = window.label().to_string(); tokio::spawn(async move { while let Some(ev) = rx.recv().await { let payload = match &ev { PipelineEvent::Progress(s) => FrontendEvent::Progress { script_name: script_name.clone(), status: s.clone(), }, PipelineEvent::Log { level, msg, fields, } => { log_node_event(&script_name, level, msg, fields); FrontendEvent::Log { script_name: script_name.clone(), level: level.clone(), msg: msg.clone(), fields: fields.clone(), } } }; let _ = app.emit_to(&win_label, EVENT_NAME, payload); } }) } /// 按脚本名跑:去后端拉 `scriptName`,包装 runner,spawn 子进程,回送结果。 #[tauri::command] pub async fn run_nodejs_script( app: AppHandle, window: Window, app_config: tauri::State<'_, AppConfig>, script_name: String, params: Value, ) -> Result { let (tx, rx) = mpsc::unbounded_channel::(); let pump = spawn_event_pump(app, window, script_name.clone(), rx); let config_env = app_config.env_for_node().await; let result = nodejs::run_node_script(script_name, params, tx, config_env) .await .map_err(|e| e.to_string())?; pump.await.ok(); Ok(result) } /// 调试:直接把传入的 source 当用户脚本喂给 runner,不走后端。 #[tauri::command] pub async fn run_nodejs_script_source( app: AppHandle, window: Window, app_config: tauri::State<'_, AppConfig>, script_source: String, params: Value, ) -> Result { let (tx, rx) = mpsc::unbounded_channel::(); let pump = spawn_event_pump(app, window, "".to_string(), rx); let config_env = app_config.env_for_node().await; let result = nodejs::run_node_script_source(script_source, params, tx, config_env) .await .map_err(|e| e.to_string())?; pump.await.ok(); Ok(result) } /// 列出后端 `scripts/nodejs` 目录下的 `.js` 文件名。 #[tauri::command] pub async fn list_nodejs_scripts() -> Result, String> { nodejs::list_scripts().await.map_err(|e| e.to_string()) }