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 配置及数据库设计文档
This commit is contained in:
fmq
2026-07-28 21:54:02 +08:00
parent 4b4238d702
commit b91f1e4fa5
59 changed files with 7697 additions and 1490 deletions
+72 -16
View File
@@ -6,11 +6,11 @@ use crate::gen_input5::make_input5;
use crate::models::{GridPointParams, ModelSummary, StageSummary, TaskType};
use crate::nst_writer::generate_nst_content;
use anyhow::Result;
use tokio::fs::File;
use std::path::{Path, PathBuf};
use std::process::Stdio;
use tokio::process::Command as AsyncCommand;
use std::time::Instant;
use tokio::fs::File;
use tokio::process::Command as AsyncCommand;
use tracing::{info, warn};
pub fn default_cold_chain() -> Vec<StageConfig> {
@@ -130,7 +130,15 @@ impl<'a> ExecutionRunner<'a> {
seed_atmos: Option<&Path>,
synspec_cfg: Option<&SynspecConfig>,
) -> Result<ModelSummary> {
self.run_model_with_timeout(params, task_type, custom_chain, seed_atmos, synspec_cfg, 7200).await
self.run_model_with_timeout(
params,
task_type,
custom_chain,
seed_atmos,
synspec_cfg,
7200,
)
.await
}
pub async fn run_model_with_timeout(
@@ -157,7 +165,9 @@ impl<'a> ExecutionRunner<'a> {
#[cfg(unix)]
{
let abs_data_dir = tokio::fs::canonicalize(&self.runtime.data_dir).await.unwrap_or_else(|_| self.runtime.data_dir.clone());
let abs_data_dir = tokio::fs::canonicalize(&self.runtime.data_dir)
.await
.unwrap_or_else(|_| self.runtime.data_dir.clone());
if let Err(e) = std::os::unix::fs::symlink(&abs_data_dir, &link_data) {
warn!("构建 data 数据集软链时发生提示性告警: {}", e);
}
@@ -171,7 +181,10 @@ impl<'a> ExecutionRunner<'a> {
if let Some(seed_path) = seed_atmos {
if seed_path.is_file() {
if let Err(e) = tokio::fs::copy(seed_path, &fort8).await {
warn!("向工作沙盒引导填载首期收敛模型种子 fort.8 发生复制错误: {}", e);
warn!(
"向工作沙盒引导填载首期收敛模型种子 fort.8 发生复制错误: {}",
e
);
}
}
}
@@ -219,15 +232,24 @@ impl<'a> ExecutionRunner<'a> {
} else if let Some(ref s_path) = current_seed {
if s_path.is_file() {
if let Err(e) = tokio::fs::copy(s_path, &fort8).await {
warn!("阶段 {} 重载候选近邻推算种子模型期间发生文件复制异常: {}", stage_def.label, e);
warn!(
"阶段 {} 重载候选近邻推算种子模型期间发生文件复制异常: {}",
stage_def.label, e
);
}
}
}
// Run tlusty.exe
let fin = File::open(&input5_path).await?.into_std().await;
let fout = File::create(model_dir.join(format!("{}.6", name))).await?.into_std().await;
let ferr = File::create(model_dir.join(format!("{}.err", name))).await?.into_std().await;
let fout = File::create(model_dir.join(format!("{}.6", name)))
.await?
.into_std()
.await;
let ferr = File::create(model_dir.join(format!("{}.err", name)))
.await?
.into_std()
.await;
let child = AsyncCommand::new(&self.runtime.tlusty_exe)
.current_dir(&model_dir)
@@ -246,7 +268,6 @@ impl<'a> ExecutionRunner<'a> {
}
};
let fort9 = model_dir.join("fort.9");
let fort7 = model_dir.join("fort.7");
@@ -293,7 +314,10 @@ impl<'a> ExecutionRunner<'a> {
stage_summaries.push(stage_summary);
if !final_converged && stage_def.require_converged {
warn!("阶段 {} 要求收敛但未达标,中止后续收敛链阶段", stage_def.label);
warn!(
"阶段 {} 要求收敛但未达标,中止后续收敛链阶段",
stage_def.label
);
break;
}
}
@@ -344,14 +368,19 @@ impl<'a> ExecutionRunner<'a> {
#[cfg(unix)]
{
let abs_linelist = tokio::fs::canonicalize(&self.runtime.linelist).await.unwrap_or_else(|_| self.runtime.linelist.clone());
let abs_linelist = tokio::fs::canonicalize(&self.runtime.linelist)
.await
.unwrap_or_else(|_| self.runtime.linelist.clone());
let _ = std::os::unix::fs::symlink(&abs_linelist, &fort19_path);
}
let input5_path = model_dir.join(format!("{}.5", name));
if input5_path.is_file() {
let fin = File::open(&input5_path).await?.into_std().await;
let fout = File::create(model_dir.join(format!("{}.log", name))).await?.into_std().await;
let fout = File::create(model_dir.join(format!("{}.log", name)))
.await?
.into_std()
.await;
let child = AsyncCommand::new(&self.runtime.synspec_exe)
.current_dir(&model_dir)
@@ -361,7 +390,8 @@ impl<'a> ExecutionRunner<'a> {
.kill_on_drop(true)
.spawn()?;
let status_res = run_child_async_with_timeout(child, timeout_sec).await;
let synspec_timeout_sec = 600_u64.min(timeout_sec);
let status_res = run_child_async_with_timeout(child, synspec_timeout_sec).await;
let rc = match status_res {
Ok(st) => st.code().unwrap_or(-1),
Err(e) => {
@@ -374,13 +404,25 @@ impl<'a> ExecutionRunner<'a> {
// Copy/move outputs: fort.7 (Synspec spectrum) -> .spec, fort.17 -> .cont, fort.12 -> .iden
if model_dir.join("fort.7").is_file() {
let _ = tokio::fs::rename(model_dir.join("fort.7"), model_dir.join(format!("{}.spec", name))).await;
let _ = tokio::fs::rename(
model_dir.join("fort.7"),
model_dir.join(format!("{}.spec", name)),
)
.await;
}
if model_dir.join("fort.17").is_file() {
let _ = tokio::fs::copy(model_dir.join("fort.17"), model_dir.join(format!("{}.cont", name))).await;
let _ = tokio::fs::copy(
model_dir.join("fort.17"),
model_dir.join(format!("{}.cont", name)),
)
.await;
}
if model_dir.join("fort.12").is_file() {
let _ = tokio::fs::copy(model_dir.join("fort.12"), model_dir.join(format!("{}.iden", name))).await;
let _ = tokio::fs::copy(
model_dir.join("fort.12"),
model_dir.join(format!("{}.iden", name)),
)
.await;
}
}
} else {
@@ -415,3 +457,17 @@ impl<'a> ExecutionRunner<'a> {
Ok(summary)
}
}
#[cfg(test)]
mod tests {
#[test]
fn test_synspec_timeout_calculation() {
let long_tlusty_timeout: u64 = 7200;
let synspec_timeout = 600_u64.min(long_tlusty_timeout);
assert_eq!(synspec_timeout, 600);
let short_tlusty_timeout: u64 = 300;
let synspec_timeout_short = 600_u64.min(short_tlusty_timeout);
assert_eq!(synspec_timeout_short, 300);
}
}