use super::AppState; use axum::{ extract::{Path as AxumPath, State}, http::StatusCode, response::IntoResponse, Json, }; use common::config::GridConfig; use serde::{Deserialize, Serialize}; use tracing::info; #[derive(Debug, Deserialize)] pub struct CreateWorkflowRequest { pub name: String, pub description: Option, pub config_yaml: String, } #[derive(Debug, Serialize)] pub struct ApiResponse { pub success: bool, pub message: String, pub data: Option, } pub async fn list_workflows( State(state): State, ) -> Result { match state.db.list_workflows().await { Ok(list) => Ok(( StatusCode::OK, Json(ApiResponse { success: true, message: "成功获取工作流列表".to_string(), data: Some(list), }), )), Err(e) => Err(e.into()), } } pub async fn get_workflow( State(state): State, AxumPath(name): AxumPath, ) -> Result { match state.db.get_workflow(&name).await { Ok(Some(item)) => Ok(( StatusCode::OK, Json(ApiResponse { success: true, message: "成功获取工作流详情".to_string(), data: Some(item), }), )), Ok(None) => Err(crate::api::AppError::NotFound(format!( "工作流 '{}' 未找到", name ))), Err(e) => Err(e.into()), } } /// 工作流名称白名单:仅允许字母、数字、点、下划线、连字符,长度 1-64。 /// 与 report_task/download_seed 的网格点名校验口径保持一致,从源头阻止 /// 名称携带 HTML/JS 特殊字符进入 Dashboard 渲染(存储型 XSS 根因之一)。 fn is_valid_workflow_name(name: &str) -> bool { !name.is_empty() && name.len() <= 64 && name .chars() .all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '_' || c == '-') } pub async fn save_workflow( State(state): State, Json(req): Json, ) -> Result { // 名称白名单校验(优先于 YAML 校验,拒绝携带特殊字符的名称) if !is_valid_workflow_name(&req.name) { return Err(crate::api::AppError::BadRequest( "工作流名称仅允许字母、数字、点(.)、下划线(_)、连字符(-),长度 1-64".to_string(), )); } // Validate YAML config string if let Err(e) = serde_yaml::from_str::(&req.config_yaml) { return Err(crate::api::AppError::BadRequest(format!( "无效的 YAML 配置: {}", e ))); } // 检查被编辑的工作流是否正处于激活运行中 if let Ok(Some(existing)) = state.db.get_workflow(&req.name).await { if existing.status == "running" || existing.status == "initializing" { return Err(crate::api::AppError::BadRequest( format!("工作流 '{}' 正处在运行或初始加载流程中,严禁原地覆写参数重设至 IDLE;如待变更参数请先调 API 显式触发停止后再保存", req.name) )); } } match state .db .upsert_workflow( &req.name, req.description.as_deref(), &req.config_yaml, "idle", ) .await { Ok(_) => { info!("成功注册/更新工作流配置: {}", req.name); Ok(( StatusCode::OK, Json(ApiResponse::<()> { success: true, message: format!("工作流 '{}' 保存成功", req.name), data: None, }), )) } Err(e) => Err(e.into()), } } pub async fn delete_workflow( State(state): State, AxumPath(name): AxumPath, ) -> Result { // 拦截正在运行或初始加载中的工作流删除请求 if let Ok(Some(existing)) = state.db.get_workflow(&name).await { if existing.status == "running" || existing.status == "initializing" { return Err(crate::api::AppError::BadRequest(format!( "工作流 '{}' 当前处于 '{}' 状态,无法直接删除。请先显式暂停/停止该工作流。", name, existing.status ))); } } let _ = state.queue.clear_queue_by_workflow(&name).await; match state.db.delete_workflow(&name).await { Ok(_) => Ok(( StatusCode::OK, Json(ApiResponse::<()> { success: true, message: format!("工作流 '{}' 已删除", name), data: None, }), )), Err(e) => Err(e.into()), } } pub async fn start_workflow( State(state): State, AxumPath(name): AxumPath, ) -> Result { let item = match state.db.get_workflow(&name).await { Ok(Some(item)) => item, Ok(None) => { return Err(crate::api::AppError::NotFound(format!( "工作流 '{}' 未找到", name ))) } Err(e) => return Err(e.into()), }; if item.status == "running" || item.status == "initializing" { return Err(crate::api::AppError::BadRequest(format!( "工作流 '{}' 已处在初始建立状态中或者已处于运行状态,无需且不允许进行并行重置启动", name ))); } // 通过原子性抢占更新将状态切换为 initializing,拦截同名流上的多并发调用导致的双重加载破坏性竞态 match state.db.transition_workflow_to_initializing(&name).await { Ok(false) => { return Err(crate::api::AppError::Conflict(format!( "工作流 '{}' 初始化抢占挂起异常,表明已在另一会话上下文中顺利推入启动通道", name ))); } Err(e) => return Err(e.into()), Ok(true) => {} } let grid_cfg: GridConfig = match serde_yaml::from_str(&item.config_yaml) { Ok(cfg) => cfg, Err(e) => { let _ = state.db.update_workflow_status(&name, "idle").await; return Err(crate::api::AppError::BadRequest(format!( "解析工作流 YAML 发生语法或参数解析异常: {}", e ))); } }; info!("成功占据独享启动权,开始异步启动工作流 '{}',系统将在后台进行 64/32 维深度平展开网格结构计算化推列并推送队列...", name); let bg_state = state.clone(); let bg_name = name.clone(); let bg_grid_cfg = grid_cfg; tokio::spawn(async move { match bg_state .scheduler .initialize_grid(&bg_grid_cfg, &bg_name) .await { Ok(_) => { let _ = bg_state .db .update_workflow_status(&bg_name, "running") .await; let _ = bg_state.scheduler.schedule_pending_tasks().await; } Err(e) => { tracing::warn!("工作流 {} 网格初始化中途失败,已回退为 idle;已写入的点保留,重新启动会幂等补齐: {}", bg_name, e); let _ = bg_state.db.update_workflow_status(&bg_name, "idle").await; } } }); Ok(( StatusCode::OK, Json(ApiResponse::<()> { success: true, message: format!("工作流 '{}' 已进入后台异步建立与挂载流程", name), data: None, }), )) } pub async fn stop_workflow( State(state): State, AxumPath(name): AxumPath, ) -> Result { match state.db.update_workflow_status(&name, "paused").await { Ok(_) => { // 多工作流分区:清理与重置都限定在本工作流内,避免误伤其他并发运行的工作流。 // - clear_queue_by_workflow:只删本工作流的排队任务。 // - reset_queued_grid_points_to_pending(&name):只把本工作流的 queued 点打回 pending。 let _ = state.queue.clear_queue_by_workflow(&name).await; let _ = state.db.reset_queued_grid_points_to_pending(&name).await; Ok(( StatusCode::OK, Json(ApiResponse::<()> { success: true, message: format!("工作流 '{}' 已暂停,排队任务已暂停调度", name), data: None, }), )) } Err(e) => Err(e.into()), } }