diff --git a/src-tauri/src/commands/utils.rs b/src-tauri/src/commands/utils.rs index d1ad39e2..66cce33f 100644 --- a/src-tauri/src/commands/utils.rs +++ b/src-tauri/src/commands/utils.rs @@ -4,6 +4,7 @@ use super::types::{MaaCallbackEvent, MaaState, StateChangedEvent}; use crate::ws_broadcast::{WsBroadcast, WsEvent}; +use log::{error, warn}; use std::path::PathBuf; use std::sync::Arc; use tauri::{AppHandle, Emitter, Manager}; @@ -277,3 +278,182 @@ pub fn build_launch_command( cmd } + +/// 获取当前 instance 所有已勾选 task 的状态 +/// +/// - 输出为数组,按照 instance 中 task 的顺序排列。 +/// - 数组的每项是一个包含两个字符串的数组:[任务名称(i18n), 任务状态("idle","pending","running","succeeded","failed")]。 +pub fn get_checked_task_status_of_instance( + app_handle: Option<&AppHandle>, + instance_id: Option<&str>, +) -> Vec> { + let app_config_state = + match app_handle.and_then(|app| app.try_state::>()) { + Some(state) => state, + None => { + error!("[MXU_STATUS] fail to get resource [app_config_state]"); + return vec![]; + } + }; + let translations = match app_config_state.translations.lock() { + Ok(guard) => guard, + Err(e) => { + error!( + "[MXU_STATUS] fail to lock resource [app_config_state.translations]: {:?}", + e + ); + return vec![]; + } + }; + let config = match app_config_state.config.lock() { + Ok(guard) => guard, + Err(e) => { + error!( + "[MXU_STATUS] fail to lock resource [app_config_state.config]: {:?}", + e + ); + return vec![]; + } + }; + + // i18n + let language = config + .get("settings") + .and_then(|v| v.get("language")) + .and_then(|v| v.as_str()) + .unwrap_or("system"); + let i18n = translations + .get(match language { + // get i18n task name by (i18n["task..label"]) + "zh-TW" => "zh_tw", + "en-US" => "en_us", + "ja-JP" => "ja_jp", + "ko-KR" => "ko_kr", + _ => "zh_cn", + }) + .unwrap_or_default(); + + // instance (config) + let id = instance_id.unwrap_or(""); + let instance_config_list = match config.get("instances").and_then(|v| v.as_array()) { + Some(list) => list, + None => { + error!("[MXU_STATUS] config data [configs/mxu-*.json > .instances] should be [array]"); + return vec![]; + } + }; + let instance_config = match instance_config_list + .iter() + .find(|inst| inst.get("id").and_then(|v| v.as_str()) == Some(id)) + { + Some(inst) => inst, + None => { + error!( + "[MXU_STATUS] config data [configs/mxu-*.json > .instances] should contains [object] item whose [.id = \"{:?}\"]", + id + ); + return vec![]; + } + }; + + // instance (runtime) + let maa_state = + match app_handle.and_then(|app| app.try_state::>()) { + Some(state) => state, + None => { + error!("[MXU_STATUS] fail to get resource [maa_state]"); + return vec![]; + } + }; + + let instance_runtime_list = match maa_state.instances.lock() { + Ok(guard) => guard, + Err(e) => { + error!( + "[MXU_STATUS] fail to lock resource [maa_state]: {:?}", + e + ); + return vec![]; + } + }; + + let instance_runtime = match instance_runtime_list.get(id) { + Some(runtime) => runtime, + None => { + error!( + "[MXU_STATUS] runtime data [maa_state.instances[\"{:?}\"]] should be [object]", + id + ); + return vec![]; + } + }; + + return instance_runtime + .task_run_state + .pending_task_ids + .iter() + .filter_map(|&maa_task_id| { + if let Some(selected_task_id) = + instance_runtime.task_run_state.mappings.get(&maa_task_id) + { + if let Some(status) = instance_runtime + .task_run_state + .statuses + .get(selected_task_id) + { + let task_config_list = instance_config.get("tasks")?.as_array()?; + let task_config = task_config_list.iter().find(|task| { + task.get("id").and_then(|v| v.as_str()) == Some(selected_task_id.as_str()) + })?; + let task_name = task_config + .get("taskName") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()) + .unwrap_or("".to_string()); + let custom_name = task_config + .get("customName") + .and_then(|v| v.as_str()) + .unwrap_or("") + .to_string(); + let task_name_i18n = i18n + .get(format!("task.{:?}.label", task_name)) + .and_then(|v| v.as_str()) + .map(|s| s.to_string()) + .unwrap_or("".to_string()); + // custom name / task name (i18n) / task name + // => task status ("idle","pending","running","succeeded","failed") + let name: String = if !custom_name.is_empty() { + custom_name + } else if !task_name_i18n.is_empty() { + task_name_i18n + } else if !task_name.is_empty(){ + task_name + } else { + warn!( + "[MXU_STATUS] config data [configs/mxu-*.json > .instances[.id = \"{:?}\"].tasks[.id = \"{:?}\"].taskName] should be [non-empty string]", + id, + selected_task_id + ); + return None + }; + return Some(vec![name, status.to_string()]) + } else { + warn!( + "[MXU_STATUS] status not found in runtime data [maa_state.instances[\"{:?}\"].task_run_state.statuses] for task in config data [configs/mxu-*.json > .instances[.id = \"{:?}\"].tasks[.id = \"{:?}\"]", + id, + id, + selected_task_id + ); + return None + } + } else { + warn!( + "[MXU_STATUS] task not found in config data [configs/mxu-*.json > .instances[.id = \"{:?}\"] related to runtime data [maa_state.instances[\"{:?}\"].task_run_state.pending_task_ids]", + id, + id + ); + return None + } + }) + .collect(); +} diff --git a/src-tauri/src/mxu_actions.rs b/src-tauri/src/mxu_actions.rs index 3b8d6cf6..efe08f35 100644 --- a/src-tauri/src/mxu_actions.rs +++ b/src-tauri/src/mxu_actions.rs @@ -2,6 +2,7 @@ //! //! 提供 MXU 特有的自定义动作实现,如 MXU_SLEEP 等 +use base64::{engine::general_purpose, Engine as _}; use chrono::TimeZone; use log::{info, warn}; use maa_framework::custom::FnAction; @@ -200,7 +201,62 @@ const MXU_LAUNCH_ACTION: &str = "MXU_LAUNCH_ACTION"; fn mxu_launch_action_fn( _ctx: &maa_framework::context::Context, args: &maa_framework::custom::ActionArgs, + app_handle: Option<&AppHandle>, + instance_id: Option<&str>, ) -> bool { + // + // {{STATUS}} + // + // success - 任务1 + // failed - 任务2 + // pending - 任务3 + // + let task_status = + crate::commands::utils::get_checked_task_status_of_instance(app_handle, instance_id); + let status = task_status + .iter() + .map(|row| format!("{} - {}", row[1], row[0])) + .collect::>() + .join("\n"); + // + // {{S_BASE64}} + // + // c3VjY2VzcyAtIOS7u+WKoTEKZmFpbGVkIC0g5Lu75YqhMgpwZW5kaW5nIC0g5Lu75YqhMw== + // + let s_base64 = general_purpose::STANDARD.encode(&status); + // + // {{S_CSV}} + // + // NAME,STATUS + // 任务1,success + // 任务2,failed + // 任务3,pending + // + let s_csv = format!( + "NAME,STATUS\n{}", + task_status + .iter() + .map(|row| format!("{},{}", row[0], row[1])) + .collect::>() + .join("\n") + ); + // + // {{S_JSON}} + // + // [["任务1","success"],["任务2","failed"],["任务3","pending"]] + // + let s_json = match serde_json::to_string(&task_status) { + Ok(v) => v, + Err(_) => String::from("[]"), + }; + // + // {{S_JSON_BASE64}} + // + // W1si5Lu75YqhMSIsInN1Y2Nlc3MiXSxbIuS7u+WKoTIiLCJmYWlsZWQiXSxbIuS7u+WKoTMiLCJwZW5kaW5nIl1d + // + let s_json_base64 = general_purpose::STANDARD.encode(&s_json); + info!("[MXU_LAUNCH] Generated task(s) status: {}", &s_json); + let param_str = args.param; info!("[MXU_LAUNCH] Received param: {}", param_str); @@ -224,7 +280,12 @@ fn mxu_launch_action_fn( .get("args") .and_then(|v| v.as_str()) .unwrap_or("") - .to_string(); + .to_string() + .replace("{{STATUS}}", &status) + .replace("{{S_BASE64}}", &s_base64) + .replace("{{S_CSV}}", &s_csv) + .replace("{{S_JSON}}", &s_json) + .replace("{{S_JSON_BASE64}}", &s_json_base64); let wait_for_exit = json .get("wait_for_exit") @@ -878,11 +939,56 @@ pub fn register_all_mxu_actions( reg_action!(MXU_SLEEP_ACTION, mxu_sleep_action_fn); reg_action!(MXU_WAITUNTIL_ACTION, mxu_waituntil_action_fn); - reg_action!(MXU_LAUNCH_ACTION, mxu_launch_action_fn); reg_action!(MXU_WEBHOOK_ACTION, mxu_webhook_action_fn); reg_action!(MXU_NOTIFY_ACTION, mxu_notify_action_fn); reg_action!(MXU_POWER_ACTION, mxu_power_action_fn); + // launch + + let launch_app_handle = app_handle.clone(); + let launch_instance_id = instance_id.to_string(); + let launch_wrapper = move |ctx: &maa_framework::context::Context, + args: &maa_framework::custom::ActionArgs| + -> bool { + std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + mxu_launch_action_fn( + ctx, + args, + Some(&launch_app_handle), + Some(&launch_instance_id), + ) + })) + .unwrap_or_else(|e| { + let msg = if let Some(s) = e.downcast_ref::<&str>() { + s.to_string() + } else if let Some(s) = e.downcast_ref::() { + s.clone() + } else { + "Unknown panic payload".to_string() + }; + log::error!( + "[MXU] Custom action {} panicked: {}", + MXU_LAUNCH_ACTION, + msg + ); + false + }) + }; + + if let Err(e) = + resource.register_custom_action(MXU_LAUNCH_ACTION, Box::new(FnAction::new(launch_wrapper))) + { + warn!("[MXU] Failed to register {}: {:?}", MXU_LAUNCH_ACTION, e); + failed_count += 1; + } else { + info!( + "[MXU] Custom action {} registered successfully", + MXU_LAUNCH_ACTION + ); + } + + // kill process + let killproc_app_handle = app_handle.clone(); let killproc_instance_id = instance_id.to_string(); let killproc_wrapper = move |ctx: &maa_framework::context::Context, diff --git a/src/i18n/locales/en-US.ts b/src/i18n/locales/en-US.ts index d4f88073..3e13638a 100644 --- a/src/i18n/locales/en-US.ts +++ b/src/i18n/locales/en-US.ts @@ -140,7 +140,8 @@ export default { programLabel: 'Program Path', programPlaceholder: 'Enter program path or click browse...', argsLabel: 'Additional Arguments', - argsPlaceholder: 'Enter additional arguments (optional)', + argsPlaceholder: 'Enter additional arguments (optional, template variables supported)', + argsDescription: 'Template variables {{STATUS}} {{S_BASE64}} {{S_CSV}} {{S_JSON}} {{S_JSON_BASE64}} represent the real-time status of selected tasks of the current instance. See https://github.com/MistEO/MXU/blob/main/src-tauri/src/mxu_actions.rs for details.', waitLabel: 'Wait for Exit', waitDescription: 'When disabled, continues immediately after launch; when enabled, waits for the process to exit before continuing, suitable for scripts that need to complete synchronously', diff --git a/src/i18n/locales/ja-JP.ts b/src/i18n/locales/ja-JP.ts index 8cd76746..95122a37 100644 --- a/src/i18n/locales/ja-JP.ts +++ b/src/i18n/locales/ja-JP.ts @@ -280,7 +280,8 @@ export default { program: 'プログラムパス', programPlaceholder: 'プログラムパスを入力または参照...', args: '追加引数', - argsPlaceholder: '追加引数を入力(オプション)', + argsPlaceholder: '追加パラメータを入力(オプション、テンプレート変数可)', + argsDescription: 'テンプレート変数 {{STATUS}} {{S_BASE64}} {{S_CSV}} {{S_JSON}} {{S_JSON_BASE64}} は、現在のインスタンスの選択されたタスクのリアルタイム状態を表します。詳細は https://github.com/MistEO/MXU/blob/main/src-tauri/src/mxu_actions.rs を参照してください。', browse: '参照', waitForExit: '終了を待機', waitForExitHintPre: diff --git a/src/i18n/locales/ko-KR.ts b/src/i18n/locales/ko-KR.ts index b021fcf5..2941e991 100644 --- a/src/i18n/locales/ko-KR.ts +++ b/src/i18n/locales/ko-KR.ts @@ -137,7 +137,8 @@ export default { programLabel: '프로그램 경로', programPlaceholder: '프로그램 경로를 입력하거나 오른쪽 찾아보기를 클릭...', argsLabel: '추가 인수', - argsPlaceholder: '추가 인수 입력 (선택 사항)', + argsPlaceholder: '추가 인수 입력(선택 사항, 템플릿 변수 지원)', + argsDescription: '템플릿 변수 {{STATUS}} {{S_BASE64}} {{S_CSV}} {{S_JSON}} {{S_JSON_BASE64}} 는 현재 인스턴스의 선택된 모든 작업의 실시간 상태를 나타냅니다. 자세한 내용은 https://github.com/MistEO/MXU/blob/main/src-tauri/src/mxu_actions.rs 를 참조하세요.', waitLabel: '종료 대기', waitDescription: '비활성화하면 실행 후 즉시 계속합니다. 활성화하면 프로세스 종료 후 계속합니다. 스크립트 등 동기 완료가 필요한 작업에 적합합니다', diff --git a/src/i18n/locales/zh-CN.ts b/src/i18n/locales/zh-CN.ts index 0359f807..c73c816b 100644 --- a/src/i18n/locales/zh-CN.ts +++ b/src/i18n/locales/zh-CN.ts @@ -136,7 +136,8 @@ export default { programLabel: '程序路径', programPlaceholder: '输入程序路径或点击右侧浏览...', argsLabel: '附加参数', - argsPlaceholder: '输入附加参数(可选)', + argsPlaceholder: '输入附加参数(可选,支持模板变量)', + argsDescription: '模板变量 {{STATUS}} {{S_BASE64}} {{S_CSV}} {{S_JSON}} {{S_JSON_BASE64}} 表示 当前实例-所有选定任务-实时状态,详见 https://github.com/MistEO/MXU/blob/main/src-tauri/src/mxu_actions.rs 。', waitLabel: '等待退出', waitDescription: '禁用时启动进程后立即继续;启用时等待进程退出后再继续工作', waitYes: '等待程序退出后继续', diff --git a/src/i18n/locales/zh-TW.ts b/src/i18n/locales/zh-TW.ts index f954a9ab..b51cdcfa 100644 --- a/src/i18n/locales/zh-TW.ts +++ b/src/i18n/locales/zh-TW.ts @@ -135,7 +135,8 @@ export default { programLabel: '程式路徑', programPlaceholder: '輸入程式路徑或點擊右側瀏覽...', argsLabel: '附加參數', - argsPlaceholder: '輸入附加參數(可選)', + argsPlaceholder: '輸入附加參數(可選,支持模板變量)', + argsDescription: '模板變量 {{STATUS}} {{S_BASE64}} {{S_CSV}} {{S_JSON}} {{S_JSON_BASE64}} 表示 當前實例-所有選定任務-即時狀態,詳見 https://github.com/MistEO/MXU/blob/main/src-tauri/src/mxu_actions.rs 。', waitLabel: '等待退出', waitDescription: '禁用時啟動程序後立即繼續;啟用時等待程序退出後再繼續,適用於執行腳本等需要同步完成的操作', diff --git a/src/types/specialTasks.ts b/src/types/specialTasks.ts index 582bc394..4d3d52b1 100644 --- a/src/types/specialTasks.ts +++ b/src/types/specialTasks.ts @@ -191,6 +191,7 @@ const MXU_LAUNCH_INPUT_OPTION_DEF_INTERNAL: InputOption = { default: '', pipeline_type: 'string', placeholder: 'specialTask.launch.argsPlaceholder', + description: 'specialTask.launch.argsDescription', }, ], pipeline_override: {