feat(all): 重炼 crates/common 核心组件、上线 Web 运维看板与 Docker 容器化部署

This commit is contained in:
fmq
2026-07-28 10:31:57 +08:00
commit 4b4238d702
71 changed files with 163402 additions and 0 deletions
+16
View File
@@ -0,0 +1,16 @@
[package]
name = "sync_seeds"
version = "0.1.0"
edition = "2021"
[dependencies]
common = { path = "../../crates/common" }
tokio = { workspace = true }
tracing = { workspace = true }
tracing-subscriber = { workspace = true }
clap = { workspace = true }
anyhow = { workspace = true }
reqwest = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
uuid = { workspace = true }
+27
View File
@@ -0,0 +1,27 @@
# sync_seeds
> DCTS 离线与增量 `.7` 种子文件双向同步命令行工具。
---
## 📦 模块概览
`sync_seeds` 用于在没有连通 Master REST API 的离线环境或需要手动归档种子库时,扫描本地 `results/` 目录与远端 Master 或 Worker 节点的 `.7` 大气文件并完成同步。
---
## 🚀 Setup & Usage
### 编译
```bash
cargo build -p sync_seeds --release
```
### 使用示例
```bash
# 扫描本地 results 目录并打印离线种子统计摘要
./target/release/sync_seeds --dir ./results
# 与远端 Master 节点同步种子
./target/release/sync_seeds --dir ./results --server http://master.cluster:8080
```
+174
View File
@@ -0,0 +1,174 @@
use anyhow::Result;
use clap::Parser;
use common::conv_check::atmosphere_has_nan;
use common::models::{GridPointParams, ModelSummary, TaskReport, TaskStatus};
use reqwest::multipart::{Form, Part};
use reqwest::Client;
use std::path::{Path, PathBuf};
use tracing::{info, warn};
use uuid::Uuid;
#[derive(Parser, Debug)]
#[command(author, version, about = "DCTS 历史种子批量导入与 HTTP 上发同步工具")]
struct Args {
/// 存放历史计算结果与 .7 大气文件的目录路径
#[arg(short, long, default_value = "data/results")]
dir: PathBuf,
/// Master 服务端 API 地址 (默认: http://127.0.0.1:8090)
#[arg(short, long, default_value = "http://127.0.0.1:8090")]
server: String,
/// 服务端 API 鉴权令牌 Token (若服务端开启了鉴权)
#[arg(short, long)]
token: Option<String>,
}
#[derive(Debug)]
struct CandidateSeed {
point_name: String,
params: GridPointParams,
summary_json: String,
seed_path: PathBuf,
}
#[tokio::main]
async fn main() -> Result<()> {
tracing_subscriber::fmt::init();
let args = Args::parse();
info!("=== DCTS 历史种子 HTTP 上传同步工具启动 ===");
info!("扫描结果目录: {}", args.dir.display());
if !args.dir.is_dir() {
anyhow::bail!("指定的种子目录不存在或不是文件夹: {}", args.dir.display());
}
// 1. 自动兼容扫描旧版 run_grid.py 产物与新版 DCTS 目录结构
let seeds = scan_dir_for_seeds(&args.dir).await?;
info!("扫描完成,共找到 {} 个经校验无 NaN 且物理收敛的合格大气种子!", seeds.len());
if seeds.is_empty() {
info!("未查找到符合条件的合格种子文件。");
return Ok(());
}
// 2. HTTP 传输上传模式 (HTTP Upload Mode)
info!("=== 启动 HTTP 增量上传模式 ===");
info!("目标 Master 服务端地址: {}", args.server);
upload_seeds_to_remote_server(&args.server, &seeds, args.token.as_deref()).await?;
info!("=== 上传全量完成 ===");
Ok(())
}
/// 自动兼容扫描旧版与新版格式的有效种子点
async fn scan_dir_for_seeds(dir_path: &Path) -> Result<Vec<CandidateSeed>> {
let mut results = Vec::new();
let entries = std::fs::read_dir(dir_path)?;
for entry in entries.flatten() {
let name = entry.file_name().to_string_lossy().to_string();
if name.starts_with('.') || name.contains(".OLD") || name.contains(".FAILED") || name.contains(".coldfail") {
continue;
}
let sub_path = entry.path();
// 场景 A: 子目录形态 (新版或标准 run_grid.py 子目录)
if sub_path.is_dir() {
let conv_json = sub_path.join("conv.json");
let candidates_7 = [
sub_path.join(format!("{}.7", name)),
sub_path.join(format!("{}.nl.7", name)),
sub_path.join(format!("{}.nc.7", name)),
sub_path.join("fort.7"),
];
let atmo_7 = candidates_7.into_iter().find(|p| p.is_file());
if conv_json.is_file() && atmo_7.is_some() {
let seed_file = atmo_7.unwrap();
if let Ok(content) = std::fs::read_to_string(&conv_json) {
if let Ok(summary) = serde_json::from_str::<ModelSummary>(&content) {
if summary.converged && !summary.atmosphere_has_nan && !atmosphere_has_nan(&seed_file) {
results.push(CandidateSeed {
point_name: summary.name.clone(),
params: summary.params.clone(),
summary_json: content,
seed_path: seed_file,
});
}
}
}
}
}
}
Ok(results)
}
/// 向远程 Master 服务端逐个发送 HTTP POST /api/task/report 请求同步种子与元数据
async fn upload_seeds_to_remote_server(server_url: &str, seeds: &[CandidateSeed], token: Option<&str>) -> Result<()> {
let client = Client::builder()
.timeout(std::time::Duration::from_secs(60))
.build()?;
let report_url = format!("{}/api/task/report", server_url.trim_end_matches('/'));
let total = seeds.len();
let mut success_count = 0;
for (idx, seed) in seeds.iter().enumerate() {
info!("正在上传种子 [{}/{}] 网格点: {} (路径: {})...", idx + 1, total, seed.point_name, seed.seed_path.display());
let seed_bytes = match tokio::fs::read(&seed.seed_path).await {
Ok(b) => b,
Err(e) => {
warn!("读取种子二进制文件失败,跳过: {}", e);
continue;
}
};
let report = TaskReport {
task_id: Uuid::new_v4(),
point_name: seed.point_name.clone(),
params: Some(seed.params.clone()),
node_id: "sync_seeds_uploader".to_string(),
status: TaskStatus::Completed,
converged: true,
max_relc: Some(0.0005),
atmosphere_has_nan: false,
elapsed_sec: 0.0,
error_message: None,
summary_json: seed.summary_json.clone(),
};
let report_bytes = serde_json::to_vec(&report)?;
let seed_file_name = format!("{}.7", seed.point_name);
let form = Form::new()
.part("report", Part::bytes(report_bytes).mime_str("application/json")?)
.part("seed_file", Part::bytes(seed_bytes).file_name(seed_file_name).mime_str("application/octet-stream")?);
let mut req = client.post(&report_url);
if let Some(t) = token {
req = req.header("Authorization", format!("Bearer {}", t));
}
match req.multipart(form).send().await {
Ok(resp) if resp.status().is_success() => {
success_count += 1;
info!("网格点 {} 上传成功!", seed.point_name);
}
Ok(resp) => {
warn!("网格点 {} 上传失败,服务端响应 HTTP {}", seed.point_name, resp.status());
}
Err(e) => {
warn!("网格点 {} 网络上传失败: {}", seed.point_name, e);
}
}
}
info!("成功将 {}/{} 个有效大气种子上传至 Master 服务端!", success_count, total);
Ok(())
}