DCTS/crates/server/src/api/workflow.rs
Asfmq b91f1e4fa5 feat(server,dashboard): 引入多工作流数据隔离、安全中间件与前端 ESM 模块化重构
- server: 实现按 workflow_name 的多工作流数据隔离与旧数据库平滑迁移机制
- server: 新增 API Key 认证(auth)、限流中间件(rate_limit)与运维备份接口(admin)
- server: 统一 AppError 错误处理体系,重构调度器 scheduler 支持工作流级重置与抢占
- node: 节点 ID 缺失时自动生成随机 UUID,原生支持 `docker compose --scale node=N` 动态扩容
- dashboard: 前端模块化重构(state/api/components),升级 CSS 变量设计系统与 Toast 通知
- docker/docs: 更新 /healthz 健康检查、部署脚本 IP 配置及数据库设计文档
2026-07-28 21:54:02 +08:00

259 lines
8.8 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<String>,
pub config_yaml: String,
}
#[derive(Debug, Serialize)]
pub struct ApiResponse<T> {
pub success: bool,
pub message: String,
pub data: Option<T>,
}
pub async fn list_workflows(
State(state): State<AppState>,
) -> Result<impl IntoResponse, crate::api::AppError> {
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<AppState>,
AxumPath(name): AxumPath<String>,
) -> Result<impl IntoResponse, crate::api::AppError> {
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<AppState>,
Json(req): Json<CreateWorkflowRequest>,
) -> Result<impl IntoResponse, crate::api::AppError> {
// 名称白名单校验(优先于 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::<GridConfig>(&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<AppState>,
AxumPath(name): AxumPath<String>,
) -> Result<impl IntoResponse, crate::api::AppError> {
// 拦截正在运行或初始加载中的工作流删除请求
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<AppState>,
AxumPath(name): AxumPath<String>,
) -> Result<impl IntoResponse, crate::api::AppError> {
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<AppState>,
AxumPath(name): AxumPath<String>,
) -> Result<impl IntoResponse, crate::api::AppError> {
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()),
}
}