- 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 配置及数据库设计文档
259 lines
8.8 KiB
Rust
259 lines
8.8 KiB
Rust
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()),
|
||
}
|
||
}
|