核心变更:
1. GridAxisValue 源精度命名
- 新增 GridAxisValue 类型,携带 f64 数值 + YAML 源书写文本(Deref 透明兼容算术)
- config.rs 绕过 serde_yaml 归一化,逐 token 捕获轴值原文(logg: 5.0 → g5.0)
- runner/executor/scheduler 全链路改用 DB TEXT 列权威 point_name,
修复 REAL 列回读丢精度导致的 model_name 错配
2. 工作流执行可观测台
- 新增 stats/progress/points 三组 API(进度时间序列、经验速率 ETA、
停滞预警、逐点明细分页、收敛性热力图数据)
- 新增 workflow_progress_snapshots 表 + tasks/grid_points 耗时列
- runner 携带 last_iter/worst_depth/n_depths 进 conv.json
- 前端新增 hash 路由、工作流详情页(概览/网格点/收敛分析三 Tab)、YAML 编辑器
3. 节点停用/启用管理
- 新增 disabled 状态 + disable/enable API;停用节点保持心跳但停止分发,
worker 空闲待命而非退出;移除 revoke API,token 失效统一走重发覆盖;
移除 host_name 字段
4. 白名单结果归档
- 新增 result_filter 模块,只归档有语义产物,丢弃 Tlusty 中间单元(~2MB/模型)
- executor 原子写入归档 + 200 点 LRU 上限
5. 历史数据导入
- sync_seeds 重写为 import_results:经 /admin/import_seed 标记 converged +
按新版命名迁移产物树
6. 部署与目录重规划
- data/results→seeds、data/archive→result + migrate_data_dirs.sh
- deploy.sh 增强(SSH 复用、Profile、远程 env);Dockerfile 瘦身
7. 文档同步更新 api/database/architecture/deployment
148 lines
5.1 KiB
Rust
148 lines
5.1 KiB
Rust
use super::{is_valid_node_id, AppState, AuthenticatedNode};
|
|
use axum::{
|
|
extract::{Extension, State},
|
|
http::StatusCode,
|
|
response::IntoResponse,
|
|
Json,
|
|
};
|
|
use common::models::{NodeHeartbeatRequest, NodeRegisterRequest};
|
|
use serde_json::json;
|
|
use tracing::{info, warn};
|
|
|
|
pub async fn register_node(
|
|
State(state): State<AppState>,
|
|
auth_node: Option<Extension<AuthenticatedNode>>,
|
|
Json(req): Json<NodeRegisterRequest>,
|
|
) -> Result<impl IntoResponse, crate::api::AppError> {
|
|
// 入口白名单校验
|
|
if !is_valid_node_id(&req.node_id) {
|
|
return Err(crate::api::AppError::BadRequest(
|
|
"非法的节点 ID(仅允许字母、数字、点、下划线、连字符,长度 1-128)".to_string(),
|
|
));
|
|
}
|
|
|
|
// 已认证已拿到 Token 的节点刷新元数据配置
|
|
if let Some(Extension(ref auth)) = auth_node {
|
|
if auth.node_id == req.node_id {
|
|
let _ = state.db.register_node(&req).await;
|
|
info!("已授权节点 {} 刷新配置成功", req.node_id);
|
|
return Ok((
|
|
StatusCode::OK,
|
|
Json(json!({
|
|
"status": "approved",
|
|
"message": "节点配置更新成功",
|
|
"node_token": null,
|
|
})),
|
|
));
|
|
}
|
|
}
|
|
|
|
// 申请注册新节点(免凭据提交申请,进入 pending_approval 状态)
|
|
match state.db.register_node(&req).await {
|
|
Ok(true) => {
|
|
info!(
|
|
"接收到新节点 {} 的注册申请,已加入待审批 (pending_approval) 队列",
|
|
req.node_id
|
|
);
|
|
Ok((
|
|
StatusCode::OK,
|
|
Json(json!({
|
|
"status": "pending_approval",
|
|
"message": "节点注册申请已成功提交!请在管理 Dashboard 控制台上点击【同意接入】授权该节点",
|
|
"node_token": null,
|
|
})),
|
|
))
|
|
}
|
|
Ok(false) => {
|
|
// 节点已处于待审批或已存在列表
|
|
Ok((
|
|
StatusCode::OK,
|
|
Json(json!({
|
|
"status": "pending_approval",
|
|
"message": "节点注册申请等待管理员审批中",
|
|
"node_token": null,
|
|
})),
|
|
))
|
|
}
|
|
Err(e) => Err(e.into()),
|
|
}
|
|
}
|
|
|
|
#[derive(serde::Deserialize)]
|
|
pub struct CheckNodeStatusRequest {
|
|
pub node_id: String,
|
|
}
|
|
|
|
/// POST /api/node/check_status — Node 端轮询检查审批结果。
|
|
pub async fn check_node_status(
|
|
State(state): State<AppState>,
|
|
Json(req): Json<CheckNodeStatusRequest>,
|
|
) -> Result<impl IntoResponse, crate::api::AppError> {
|
|
if !is_valid_node_id(&req.node_id) {
|
|
return Err(crate::api::AppError::BadRequest(
|
|
"非法的节点 ID 参数".to_string(),
|
|
));
|
|
}
|
|
|
|
// 尝试拉取取走即焚的暂存明文 Token
|
|
match state.db.take_pending_node_token(&req.node_id).await {
|
|
Ok(Some(raw_token)) => {
|
|
info!(
|
|
"节点 {} 的注册申请已被管理员审批同意,下发专属 Token",
|
|
req.node_id
|
|
);
|
|
Ok((
|
|
StatusCode::OK,
|
|
Json(json!({
|
|
"status": "approved",
|
|
"message": "节点已通过审批授权",
|
|
"node_token": raw_token,
|
|
})),
|
|
))
|
|
}
|
|
Ok(None) | Err(_) => {
|
|
// 查节点表状态
|
|
match state.db.get_node_exists(&req.node_id).await {
|
|
Ok(true) => Ok((
|
|
StatusCode::OK,
|
|
Json(json!({
|
|
"status": "pending_approval",
|
|
"message": "等待管理员在控制台点击同意",
|
|
"node_token": null,
|
|
})),
|
|
)),
|
|
_ => Ok((
|
|
StatusCode::OK,
|
|
Json(json!({
|
|
"status": "rejected",
|
|
"message": "节点注册申请未通过或已被移除",
|
|
"node_token": null,
|
|
})),
|
|
)),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub async fn heartbeat_node(
|
|
State(state): State<AppState>,
|
|
Extension(auth_node): Extension<AuthenticatedNode>,
|
|
Json(req): Json<NodeHeartbeatRequest>,
|
|
) -> Result<impl IntoResponse, crate::api::AppError> {
|
|
// 身份绑定校验:请求体声称的 node_id 必须与 token 绑定的 node_id 一致,
|
|
// 杜绝「持有 A 节点 token 却冒充 B 节点发心跳」的跨节点越权。
|
|
if req.node_id != auth_node.node_id {
|
|
warn!(
|
|
"节点心跳身份校验失败:token 绑定 node={},但请求体声称 node_id={}",
|
|
auth_node.node_id, req.node_id
|
|
);
|
|
return Err(crate::api::AppError::Forbidden(
|
|
"node_id 与凭据不匹配".to_string(),
|
|
));
|
|
}
|
|
match state.db.heartbeat_node(&req).await {
|
|
Ok(_) => Ok(Json(json!({"status": "ok"}))),
|
|
Err(e) => Err(e.into()),
|
|
}
|
|
}
|