feat(all): 源精度命名体系、工作流可观测台、节点停用管理与白名单归档

核心变更:

  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
This commit is contained in:
fmq
2026-07-31 01:34:05 +08:00
parent b91f1e4fa5
commit 1bfa240cb0
73 changed files with 12332 additions and 1608 deletions
+310 -32
View File
@@ -29,7 +29,7 @@
服务端基于角色访问控制 (RBAC) 划分三种请求鉴权级别:
1. **Admin 角色**:具有系统管理权限(工作流 CRUD/起停、节点凭据审批/吊销/重发、系统恢复)。在 Request Header 中需携带:
1. **Admin 角色**:具有系统管理权限(工作流 CRUD/起停、节点凭据审批/重发/停用/启用、系统恢复)。在 Request Header 中需携带:
```http
Authorization: Bearer <DCTS_ADMIN_TOKEN>
# 或
@@ -63,19 +63,29 @@
### 2.1 6维网格点参数 (`GridPointParams`)
定义恒星大气模型的 6 维大气参数:温度 $T_{\text{eff}}$、重力加速度 $\log g$、以及元素丰度 $\log(N_{\text{He}}/N_{\text{H}})$, $\log(N_{\text{C}}/N_{\text{H}})$, $\log(N_{\text{N}}/N_{\text{H}})$, $\log(N_{\text{O}}/N_{\text{H}})$。
- **Rust 定义** ([models.rs](file:///home/fmq/program/tlusty/tl208-s54/dcts/crates/common/src/models.rs#L6-L14)):
- **Rust 定义** ([models.rs](file:///home/fmq/program/tlusty/tl208-s54/dcts/crates/common/src/models.rs)):
```rust
// 每个轴值携带数值与 YAML 源书写文本,使命名严格忠于源精度。
#[derive(Debug, Clone, PartialEq)]
pub struct GridAxisValue { /* 内部:value: f64 + text: 源文本 */ }
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct GridPointParams {
pub teff: f64, // 有效温度 (K), e.g. 35000.0
pub logg: f64, // 表面重力加速度对数 (cgs), e.g. 5.5
pub loghe: f64, // 氦丰度对数, e.g. -1.0
pub logc: f64, // 碳丰度对数, e.g. -2.0
pub logn: f64, // 氮丰度对数, e.g. -2.0
pub logo: f64, // 氧丰度对数, e.g. -2.0
pub teff: GridAxisValue, // 有效温度 (K), e.g. 35000.0
pub logg: GridAxisValue, // 表面重力加速度对数 (cgs), e.g. 5.5
pub loghe: GridAxisValue, // 氦丰度对数, e.g. -1.0
pub logc: GridAxisValue, // 碳丰度对数, e.g. -2.0
pub logn: GridAxisValue, // 氮丰度对数, e.g. -2.0
pub logo: GridAxisValue, // 氧丰度对数, e.g. -2.0
}
```
> **serde 行为**`GridAxisValue` 序列化为纯数值(`f64`),下游 JSON 消费者无感;
> 反序列化时优先捕获 YAML/JSON 标量原文。**命名精度**`model_name()` 直接拼接各轴
> 源书写文本——配置里多少位小数就多少位(`logg: 5.0` → `g5.0``teff: 20000` → `t20000`),
> 与旧版 Python `gen_input5.model_name` 逐字符一致,保证历史数据可迁移。详见
> [`GridConfig::from_yaml_str`](file:///home/fmq/program/tlusty/tl208-s54/dcts/crates/common/src/config.rs)YAML 源文本捕获路径)。
- **TypeScript 类型声明**:
```typescript
export interface GridPointParams {
@@ -188,7 +198,6 @@ Worker 节点向服务端上报的任务计算结果。
```rust
pub struct NodeRegisterRequest {
pub node_id: String,
pub host_name: String,
pub max_slots: i32,
}
@@ -201,7 +210,6 @@ Worker 节点向服务端上报的任务计算结果。
pub struct NodeInfo {
pub node_id: String,
pub host_name: String,
pub max_slots: i32,
pub active_slots: i32,
pub status: String,
@@ -215,7 +223,6 @@ Worker 节点向服务端上报的任务计算结果。
```typescript
export interface NodeRegisterRequest {
node_id: string;
host_name: string;
max_slots: number;
}
@@ -228,7 +235,6 @@ Worker 节点向服务端上报的任务计算结果。
export interface NodeInfo {
node_id: string;
host_name: string;
max_slots: number;
active_slots: number;
status: 'online' | 'offline';
@@ -237,6 +243,7 @@ Worker 节点向服务端上报的任务计算结果。
last_heartbeat: string;
}
```
> **节点 `status` 取值**: `'online'`(在线分发中)、`'offline'`(心跳超时离线)、`'pending_approval'`(待管理员审批)、`'disabled'`(管理员手动停用——在线但不分发任务,详见 [`/disable`](#6-停用节点-post-apiadminnodesnode_iddisable))。
---
@@ -302,42 +309,42 @@ Worker 节点向服务端上报的任务计算结果。
```rust
pub async fn register_node(
State(state): State<AppState>,
Json(req): Json<NodeRegisterRequest>,
auth_node: Option<Extension<AuthenticatedNode>>,
Json(req): Json<NodeRegisterRequest>
) -> impl IntoResponse
```
- **鉴权**: 是 (若配置 Token)
- **鉴权**: 否(免凭据申请,提交后进入 `pending_approval` 待管理员审批)
- **请求 Header**: `Content-Type: application/json`
- **请求 Body**:
```json
{
"node_id": "node-worker-01",
"host_name": "hpc-node-01.local",
"max_slots": 8
}
```
- **响应 Schema**:
- `200 OK` (成功):
- `200 OK` (新节点申请提交成功,待审批):
```json
{
"status": "ok",
"message": "节点注册成功"
"status": "pending_approval",
"message": "节点注册申请已成功提交!请在管理 Dashboard 控制台上点击【同意接入】授权该节点",
"node_token": null
}
```
- `200 OK` (数据库异常):
- `200 OK` (已授权节点带专属 token 刷新配置):
```json
{
"status": "error",
"message": "数据库错误详情"
"status": "approved",
"message": "节点配置更新成功",
"node_token": null
}
```
- **curl 示例**:
```bash
curl -X POST http://localhost:8090/api/node/register \
-H "Content-Type: application/json" \
-H "Authorization: Bearer secret_token" \
-d '{
"node_id": "node-worker-01",
"host_name": "hpc-node-01.local",
"max_slots": 8
}'
```
@@ -406,13 +413,14 @@ Worker 节点向服务端上报的任务计算结果。
"data": [
{
"node_id": "node-worker-01",
"has_token": true,
"is_revoked": false,
"issued_at": "2026-07-28T12:00:00Z"
"status": "online",
"token_status": "active",
"token_issued_at": "2026-07-28T12:00:00Z"
}
]
}
```
> `token_status` 取值:`active`(有效)/ `none`(无凭据记录)。token 失效靠重发覆盖 hash 实现,不存在「已吊销」中间态。
#### 2. 同意节点接入申请 (`POST /api/admin/nodes/:node_id/approve`)
- **权限**: Admin 角色
@@ -422,13 +430,34 @@ Worker 节点向服务端上报的任务计算结果。
- **权限**: Admin 角色
- **说明**: 拒绝处于待审批状态的 Node 接入。
#### 4. 吊销节点专属 Token (`POST /api/admin/nodes/:node_id/revoke`)
#### 4. 重新颁发节点专属 Token (`POST /api/admin/nodes/:node_id/reissue`)
- **权限**: Admin 角色
- **说明**: 立即吊销节点专属 Token,被吊销的 Token 无法再通过 Node 鉴权,需重新申请
- **说明**: 作废旧 Token(hash 被覆盖,立即失效)并重新生成新 Token 文本返回。新明文同时暂存到服务端,节点可通过 `/node/check_status` 自动拉取(限 1 天内有效),或由管理员手动同步到节点本地 `.node_token`
#### 5. 重新颁发节点专属 Token (`POST /api/admin/nodes/:node_id/reissue`)
#### 5. 停用节点 (`POST /api/admin/nodes/:node_id/disable`)
- **权限**: Admin 角色
- **说明**: 作废旧 Token 并重新生成新 Token 文本返回
- **说明**: 手动停用一个处于 `online`/`offline` 状态的节点。停用后节点**保持在线心跳**(Dashboard 仍可见其存活),但 `claim` 不再向其分发任务,Worker 收到 `{"status":"disabled"}` 后会拉长轮询、空闲待命,可随时调用 `enable` 恢复
- **状态语义**: 节点 `status` 切为 `disabled`。与「重发 Token」(凭据失效、Worker 强制退出重注册)不同——停用仅是运维意图,不退出 Worker 进程、不动凭据。
- **响应**:
- `200 OK`: `{"success": true, "message": "节点 '...' 已停用,不再分发任务"}`
- `409 Conflict`: 节点当前状态不支持停用(仅 `online`/`offline` 可停用,`pending_approval`/`disabled` 返回此码)。
- **curl 示例**:
```bash
curl -X POST http://localhost:8090/api/admin/nodes/node-worker-01/disable \
-H "Authorization: Bearer <ADMIN_TOKEN>"
```
#### 6. 重新启用节点 (`POST /api/admin/nodes/:node_id/enable`)
- **权限**: Admin 角色
- **说明**: 重新启用被手动停用(`disabled`)的节点。状态切为 `offline`,靠节点下一次心跳自然翻为 `online` 后即恢复分发任务——既能自愈,又不会对真实离线的节点虚报在线。
- **响应**:
- `200 OK`: `{"success": true, "message": "节点 '...' 已重新启用,将在下一次心跳后恢复分发任务"}`
- `409 Conflict`: 节点当前状态不支持启用(仅 `disabled` 可启用)。
- **curl 示例**:
```bash
curl -X POST http://localhost:8090/api/admin/nodes/node-worker-01/enable \
-H "Authorization: Bearer <ADMIN_TOKEN>"
```
---
@@ -474,6 +503,14 @@ Worker 节点向服务端上报的任务计算结果。
"task": null
}
```
- `200 OK` (节点被管理员手动停用,不再分发任务):
```json
{
"status": "disabled",
"task": null
}
```
> Worker 收到此响应后保持存活、拉长轮询(每 60s)空闲待命,**不会**退出进程(区别于 401/403 的 Token 失效语义)。管理员调用 `/enable` 后下一次轮询即恢复领用。
- `500 Internal Server Error`:
```json
{
@@ -525,7 +562,7 @@ Worker 节点向服务端上报的任务计算结果。
"message": "无法解析 params 或 summary_json"
}
```
- **说明**: 当 `converged == true` 且 `atmosphere_has_nan == false` 且包含 `seed_file` 时,服务端会将种子保存至 `results_dir/<point_name>/<point_name>.7` 并记入 `seeds` 表。若冷启动任务失败,服务端会自动唤醒 `GridScheduler` 触发针对该网格点的步进回退算法 (Seed-step Fallback)。
- **说明**: 当 `converged == true` 且 `atmosphere_has_nan == false` 且包含 `seed_file` 时,服务端会将种子保存至 `seeds_dir/<point_name>/<point_name>.7` 并记入 `seeds` 表。若冷启动任务失败,服务端会自动唤醒 `GridScheduler` 触发针对该网格点的步进回退算法 (Seed-step Fallback)。
- **curl 示例**:
```bash
curl -X POST http://localhost:8090/api/task/report \
@@ -545,6 +582,44 @@ Worker 节点向服务端上报的任务计算结果。
-F 'seed_file=@/path/to/t35000_g5.5_he-1_c-2_n-2_o-2.7'
```
### 4.3 历史种子批量导入 (`POST /api/admin/import_seed`)
把旧版单机 `run_grid.py` 产物(`conv.json` + `.7` 大气文件)批量回灌进 DCTS。与 `/task/report` 的关键区别:**跳过任务归属校验**(历史数据无领用语义),直接幂等落库。供 [`tools/import_results`](file:///home/fmq/program/tlusty/tl208-s54/dcts/tools/import_results/src/main.rs) 调用。
- **处理函数**: [`import_seed`](file:///home/fmq/program/tlusty/tl208-s54/dcts/crates/server/src/api/task.rs)
- **鉴权**: **Admin 角色**admin token 或登录 session
- **请求格式**: `multipart/form-data`
- Part `report`: 旧版 `conv.json` 的**原文 JSON**(映射为 `ModelSummary`,服务端解析出 `name` / `params` / `converged` / `final_max_relc`
- Part `seed_file`: 二进制数据(`.7` 大气种子文件;收敛点必传)
- **Query 参数**: `workflow`(可选,默认 `imported`):目标工作流名,种子导入到该工作流的 `grid_points`。
- **命名保真**: `point_name` 取旧 `conv.json` 的 `name` 字段(源精度真名,如 `t20000_g5.0_...`),**逐字符**落库(磁盘目录、`grid_points.name`、`seeds.point_name`),与旧版 Python `gen_input5.model_name` 完全一致。
- **幂等**: `ON CONFLICT DO NOTHING` upsert `grid_points`、`conv.json` 与 `.7` 原子覆盖写,可重复运行。
- **响应 Schema**:
- `200 OK`:
```json
{
"status": "ok",
"point_name": "t20000_g5.0_he-2_c-4_n-4_o-4",
"converged": true,
"max_relc": 0.000321
}
```
- `400 Bad Request`: 缺少 `report` / `conv.json` 解析失败 / 非法网格点名称
- `401 Unauthorized`: 缺少或无效 admin 凭据
- **curl 示例**:
```bash
curl -X POST "http://localhost:8090/api/admin/import_seed?workflow=sdB_cno" \
-H "Authorization: Bearer <admin_token>" \
-F 'report=@/path/to/old_results/t20000_g5.0_he-2_c-4_n-4_o-4/conv.json;type=application/json' \
-F 'seed_file=@/path/to/old_results/t20000_g5.0_he-2_c-4_n-4_o-4/t20000_g5.0_he-2_c-4_n-4_o-4.7'
```
- **批量导入工具**: [`import_results`](file:///home/fmq/program/tlusty/tl208-s54/dcts/tools/import_results/src/main.rs) 自动扫描结果目录、校验无 NaN 且收敛、逐点调用本端点(同时把完整产物树迁移到 `data/result/`):
```bash
cargo run -p import_results -- --dir <旧 results 根目录> \
--config workflows/sdB_cno.yaml \
--server http://127.0.0.1:8090 --workflow sdB_cno --token <admin_token>
```
---
## 5. 种子文件管理 API (Seed Management)
@@ -656,7 +731,6 @@ Worker 节点向服务端上报的任务计算结果。
"nodes": [
{
"node_id": "node-worker-01",
"host_name": "hpc-node-01.local",
"max_slots": 8,
"active_slots": 2,
"status": "online",
@@ -930,6 +1004,210 @@ Worker 节点向服务端上报的任务计算结果。
---
### 8.7 工作流执行统计 (`GET /api/workflows/:name/stats`)
- **处理函数**: `get_workflow_stats``crates/server/src/api/workflow.rs`
- **权限**: Admin`/workflows/*` 前缀统一映射)
- **用途**: 详情页"任务控制条 + 执行概览"数据源。在 `get_grid_summary_stats(Some(wf))`
之上追加难度波次分布与近似 ETA。**`pending` 与 `queued` 分开计数**(与 `/api/status`
的全局口径不同,后者由前端合并展示)。
- **路径参数**: `:name` — 工作流名(白名单 `[A-Za-z0-9._-]{1,64}`);未知工作流 → `404`。
- **响应 Schema (`200 OK`)**:
```json
{
"success": true,
"message": "成功获取工作流统计",
"data": {
"name": "sdB_cno",
"status": "running",
"total": 432,
"pending": 120,
"queued": 40,
"running": 8,
"converged": 261,
"failed": 3,
"cold_run_converged": 220,
"seed_step_converged": 41,
"imported_converged": 0,
"waves": [
{ "wave": 0, "total": 108, "converged": 108, "failed": 0 }
],
"avg_point_sec": 740.5,
"eta_sec": 9620.0
}
}
```
> `avg_point_sec` = `AVG(COALESCE(tasks.elapsed_sec, created_at→completed_at 时间戳差))`——
> 优先用 Worker 回报的精确墙钟(不含排队等待),历史无 `elapsed_sec` 的行回退时间戳差近似;
> `eta_sec` = `avg_point_sec × (total - converged - failed) ÷ 在线节点总槽位`(并发感知;
> 无在线节点按串行兜底);无历史数据时二者为 `null`。
---
### 8.8 工作流网格点列表 (`GET /api/workflows/:name/points`)
- **处理函数**: `get_workflow_points``crates/server/src/api/workflow.rs`
- **权限**: Admin
- **用途**: 详情页"网格点明细"表与"收敛性分析"热力图、概览"最近动态"流的数据源。
每行附带**最近一次尝试**信息(`tasks` 关联子查询取最新行,从未派发过的点 `last_*` 为 null)。
- **查询参数**(全部可选,枚举值白名单校验,非法 → `400`:
| 参数 | 取值 | 默认 |
| :--- | :--- | :--- |
| `status` | `pending`/`queued`/`running`/`converged`/`failed` | 不过滤 |
| `method` | `cold_run`/`seed_step`/`imported` | 不过滤 |
| `wave` | 整数波次 | 不过滤 |
| `q` | 点名子串(LIKE 通配符已转义) | 不过滤 |
| `sort` | `wave`/`teff`/`max_relc`/`attempts`/`last_completed_at` | `wave` |
| `order` | `asc`/`desc` | `asc` |
| `limit` | 1500(超出钳位) | 100 |
| `offset` | ≥0 | 0 |
- **响应 Schema (`200 OK`)**:
```json
{
"success": true,
"message": "成功获取工作流网格点列表",
"data": {
"total": 432,
"points": [
{
"name": "t60000_g5.0_he-2_c-4_n-4_o-4",
"teff": 60000.0, "logg": 5.0, "loghe": -2.0,
"logc": -4.0, "logn": -4.0, "logo": -4.0,
"cno_sum": -12.0, "wave": 0,
"status": "converged",
"success_method": "seed_step",
"attempt_count": 2,
"last_max_relc": 0.00043,
"last_task_type": "seed_step",
"seed_point_name": "t60000_g5.0_he2_c-4_n-4_o-4",
"node_id": "node-a1b2",
"last_completed_at": "2026-07-30 11:12:00",
"last_error": null
}
]
}
}
```
---
### 8.9 网格点详情 (`GET /api/workflows/:name/points/:point`)
- **处理函数**: `get_workflow_point_detail``crates/server/src/api/workflow.rs`
- **权限**: Admin
- **用途**: 点详情滑入面板——尝试历史(还原"冷启动失败 → 种子步进救回"剧情)
+ `conv.json` 逐阶段诊断。
- **路径参数**: `:point` — 点名白名单 `[A-Za-z0-9._-+@]`(非空、拒前导 `.`、≤128
即路径穿越前置闸门);conv.json 读盘前再经 canonicalize 归属校验。非法点名 → `400`
点不存在 → `404`。
- **响应 Schema (`200 OK`)**:
```json
{
"success": true,
"message": "成功获取网格点详情",
"data": {
"point": { "...": "同 8.8 单行" },
"attempts": [
{
"task_id": "uuid",
"task_type": "cold_run",
"seed_point_name": null,
"status": "failed",
"max_relc": 954000.0,
"atmosphere_has_nan": false,
"node_id": "node-a1b2",
"error_message": "nl stage diverged",
"created_at": "2026-07-30 09:00:00",
"completed_at": "2026-07-30 09:30:00"
},
{ "task_type": "seed_step", "status": "completed", "...": "第二次尝试(救回)" }
],
"conv": {
"converged": true,
"final_max_relc": 0.00043,
"final_chmax": 0.001,
"seed": "data/seeds/<seed_name>/<seed_name>.7",
"atmosphere_has_nan": false,
"synspec_rc": 0,
"synspec_sec": 3.1,
"elapsed_sec": 126.4,
"stages": [
{ "label": "seed_nc", "chmax": 0.001, "lte": false, "converged": false, "best_max_relc": 0.02, "elapsed_sec": 62.4, "note": null },
{ "label": "nl", "chmax": 0.001, "lte": false, "converged": true, "best_max_relc": 0.00043, "elapsed_sec": 64.0, "note": null }
]
}
}
}
```
> `conv` 来自 `seeds_dir/<point>/conv.json``ModelSummary` 结构,见
> `crates/common/src/models.rs`)。文件缺失/解析失败时为 `null`(仍返回 200
> 前端降级显示"诊断文件不可用")。
---
### 8.10 工作流进度时间序列 (`GET /api/workflows/:name/progress`)
- **处理函数**: `get_workflow_progress``crates/server/src/api/workflow.rs`
- **权限**: Admin
- **用途**: 详情页概览的进度曲线(sparkline)、经验速率 ETA 与停滞预警数据源。
快照由服务端后台循环(~30s)对每个运行中工作流写入 `workflow_progress_snapshots`
表,**计数无变化不落库**(去重防膨胀),保留期 7 天自动清理。
- **查询参数**: `hours`(时间窗口,默认 24,钳位 1–168)。
- **响应 Schema (`200 OK`)**:
```json
{
"success": true,
"message": "成功获取进度时间序列",
"data": {
"hours": 24,
"series": [
{ "ts": "2026-07-31 08:00:00", "total": 432, "pending": 120, "queued": 40,
"running": 8, "converged": 261, "failed": 3 }
],
"rate_per_hour": 12.5,
"stalled_minutes": 0.0
}
}
```
> `series` 超 300 条自动降采样(首末点保留);`rate_per_hour` = 窗口首末
> converged 增量 ÷ 时长(快照不足 2 条为 null);`stalled_minutes` = 终态数
> converged+failed)最后一次增长至窗口末端的分钟数(前端 >10 分钟触发停滞预警)。
---
### 8.11 工作流列表内联统计(`GET /api/workflows` 响应增强)
`WorkflowSummary` 新增 `stats` 字段(单条 `GROUP BY` 聚合回填,无 N+1):
工作流尚无网格点(未启动)时为 `null`,前端据此不渲染卡片进度条。
```json
{
"success": true,
"message": "成功获取工作流列表",
"data": [
{
"name": "sdB_cno",
"description": "sdB 6维恒星大气模型计算网格",
"status": "running",
"created_at": "2026-07-30 10:00:00",
"updated_at": "2026-07-30 11:00:00",
"stats": {
"total": 432,
"converged": 261,
"failed": 3,
"running": 8,
"cold_run_converged": 220,
"seed_step_converged": 41
}
}
]
}
```
---
## 9. 错误处理与状态码汇总
| HTTP 状态码 | 触发场景说明 | 响应格式 | 核心原因与解决建议 |
+69 -11
View File
@@ -15,7 +15,7 @@ flowchart TB
DB[(主数据库 dcts.db)]
MQ[(任务队列 dcts_queue.db)]
Scheduler["Grid Scheduler 网格调度器"]
SeedsStorage["本地种子库 results/*.7"]
SeedsStorage["本地种子库 seeds/*.7"]
API <--> DB
API <--> MQ
@@ -33,8 +33,8 @@ flowchart TB
Worker1 -- "2. 下载种子/基础数据" --> API
Worker1 -- "3. 汇报结果/上传 .7 大气" --> API
Worker2 -- "HTTP REST / Bearer Auth" --> API
WorkerN -- "HTTP REST / Bearer Auth" --> API
Worker2 -- "免凭据注册申请 / 审批后心跳" --> API
WorkerN -- "免凭据注册申请 / 审批后心跳" --> API
API --> SeedsStorage
```
@@ -46,8 +46,8 @@ flowchart TB
### 2.1 Master 服务端 (`server` & Web `dashboard`)
- **工作流与多任务隔离**:支持多工作流并发隔离运行(`grid_points``task_queue` 增加 `workflow_name` 复合唯一索引),调度与重置操作严格隔离在单工作流作用域内。支持旧版 SQLite 数据库启动时无缝平滑迁移。
- **安全中间件与 RBAC**:基于角色访问控制 (Admin / Node / Public) 划分 API 权限,内嵌 Bearer Token 校验、Governor / 漏桶算法限流中间件与 CORS 跨域控制,防御侧信道攻击与暴力破解。
- **节点凭据生命周期管理**:支持 Worker 节点注册申请、管理员审批授权、Token 吊销与重新颁发全生命周期管理。
- **任务调度与分配**通过 `mq` 队列管理任务生命周期,响应 Worker 的 Claim 请求分配就绪任务
- **节点凭据生命周期管理**:支持 Worker 节点注册申请、管理员审批授权、Token 重新颁发(旧 token 失效)与节点停用/启用全生命周期管理。
- **任务调度与分配**调度器将 pending 网格点推入 `mq` 队列;Worker 以 **pull 抢占式** 领用任务(详见 [§5 任务调度模型](#5-任务调度模型-task-scheduling)
- **状态维护与心跳监测**:后台离线检测线程定期标记超时未心跳的节点为 `offline`,并能将僵挂在超时节点上的任务自动回收到队列中(Requeue)。
- **ESM 模块化 Web 看板**:前端采用 ESM 模块解耦设计(`state.js`, `api.js`, `components/`),支持节点凭据管理、工作流控制与全局 Toast 通知。
@@ -66,8 +66,9 @@ flowchart TB
```mermaid
stateDiagram-v2
[*] --> Pending : 工作流注册生成网格点
Pending --> Running : Worker 成功 Claim 抢占
Pending --> Queued : 调度器推入 mq 队列
Queued --> Running : Worker 成功 Claim 抢占
state Running {
[*] --> ExecutingChain
ExecutingChain --> ColdStartChain : 默认冷启动 (lte->nc->nl)
@@ -76,14 +77,18 @@ stateDiagram-v2
SeedStepChain --> Synspec : 热启动收敛
}
Running --> Completed : 计算成功 & 上传 .7 产物
Running --> Pending : Worker 节点心跳超时/主动释放 (Requeue)
Running --> Failed : 重试次数达到上限 / 彻底发散
Running --> Converged : 计算成功 & 上传 .7 产物
Running --> Pending : Worker 心跳超时/掉线 (Requeue, 直接回 Pending 等待重新派发)
Running --> Failed : 无可邻近种子回退 / 彻底发散
Completed --> [*]
Converged --> [*]
Failed --> [*]
```
> 状态值与 [`GridPointStatus`](file:///home/fmq/program/tlusty/tl208-s54/dcts/crates/common/src/models.rs#L237) 一一对应:
> `pending` → `queued` → `running` → `converged` / `failed`。
> Requeue 把 `running` 直接重置回 `pending`(不经过 `queued`),由调度器下一轮重新推入队列。
---
## 4. 容错与高可用设计 (Fault Tolerance)
@@ -92,3 +97,56 @@ stateDiagram-v2
2. **零文件扫描与连接并发缩流**:底层任务分批取配、排队清洗与邻接优化(Seed-Stepping)全链线依赖常驻内存的 SQlite 主从精算并调优收敛连接池配置(主库=8,队列=4 减免本地排他写冲突并发挂断);去除了历史残存的高损及同步阻塞磁盘遍历 API,在确保零卡死响应的前提下提升查询搜索效力。
3. **任务超时与流控平稳保护 (Stale & Requeue)**:服务端后台定期向已超时死挂的 `Running` (默认 >1800 秒)作业予以强退回转为 `Pending`;此外当操作维护员发起暂停或终止工作流行为时,将仅平滑洗退待调 `Queued` 项,悉心保育在途已投的 Worker 数值演算完整出计算归表,防假命题竞合。
4. **多级退避与种子隔离**:若某点冷启动发散,自动隔离失败现场,依靠数据库记录寻找欧氏空间距离最匹配的热启动合拢 `.7` 气象序列;即便遇到底层强硬大步发散也不产生干扰并留存物理运行根系以便溯源。
---
## 5. 任务调度模型 (Task Scheduling)
### 5.1 Pull 抢占式模型(非服务端负载分发)
DCTS 的任务分发是 **Worker 主动拉取(pull** 模型,**不是**服务端按节点负载推派(push):
- **调度器**[`schedule_pending_tasks`](file:///home/fmq/program/tlusty/tl208-s54/dcts/crates/server/src/scheduler.rs#L153))只负责把 `pending` 网格点推入 `mq` 队列(`push_task`),不参与"分给哪个 node"的决策。
- **Worker**[领用主循环](file:///home/fmq/program/tlusty/tl208-s54/dcts/crates/node/src/worker.rs#L275))只要 `active_slots < max_slots`,就持续向服务端发起 `claim` 请求抢任务。
- **服务端** [`pop_task`](file:///home/fmq/program/tlusty/tl208-s54/dcts/crates/mq/src/sqlite_queue.rs#L145) 的出队决策**只看任务自身属性**,不看谁来领用:谁先抢到写锁、谁就拿到队首任务。
出队排序(难度优先 → 同难度先进先出):
```sql
SELECT task_id, payload FROM task_queue
WHERE status = 'pending'
ORDER BY wave ASC, created_at ASC
LIMIT 1
```
### 5.2 为什么不做服务端负载分发
关键观察:**Worker 满载后会主动停止领用**([worker.rs](file:///home/fmq/program/tlusty/tl208-s54/dcts/crates/node/src/worker.rs#L280-L281)):
```rust
let active = self.active_slots.load(Ordering::Acquire);
if (active as usize) < self.config.max_slots {
match self.claim_task().await { /* 抢任务 */ }
} else {
sleep(Duration::from_secs(2)).await; // 满载:不 claim,等槽位释放
}
```
由此分两种工况:
| 工况 | 条件 | 是否会扎堆 | 负载分发是否需要 |
|------|------|-----------|-----------------|
| **稳态** | 任务数 ≥ 所有 node 总槽位 | 否——各 node 抢满 `max_slots` 即停止 claim,空出槽位者再去抢,**天然均衡** | 不需要 |
| **稀疏态** | 任务数 < 总槽位(如调试用 2 个点) | 是——最快的 node 连续抢光少量任务 | 收益极小,省的仅是几个任务的串行墙钟时间 |
稳态(网格规模任务,如 sdB_cno 的 432 点)是实际生产场景,该场景下 pull 模型已自动达成负载均衡。稀疏态的扎堆是固有现象且代价可忽略(任务跑完即结束)。
引入服务端负载分发的**代价**反而更高:破坏 FIFO 可预测性、带来心跳负载数据滞后导致的退让抖动、增加队首饥饿风险。综合权衡,**当前 pull 模型是刻意选择,无需改为服务端负载分发**。
> **稀疏态调试建议**:若调试时确实希望任务分散到多个 node 观察,最省事的做法是把各 node 的 `DCTS_MAX_SLOTS` 设为 `1`——每个 node 抢 1 个即满载停止,自然分散,零代码改动、零风险。
### 5.3 出队优先级语义(wave + FIFO
- **`wave ASC`(难度优先)**:网格点按 CNO 丰度之和(`cno_sum`)升序分组,低丰度(低难度、更易收敛)的点先出队。设计意图是"先易后难"——先跑出收敛点作为种子库,后续高难度点可借热启动种子收敛。
- **`created_at ASC`(同难度 FIFO**:同一 `wave` 内严格按推入队列的时间先后出队。
- **跨工作流公平**:出队不按 `workflow_name` 分组,多个并发工作流的低难度点会先于任何工作流的高难度点出队,避免某工作流在高难度 wave 上饿死其他工作流的低难度点。
+1 -1
View File
@@ -32,7 +32,7 @@ dcts/
│ ├── node/ # [Binary] Worker 节点 Daemon 与任务抢占回路
│ └── mq/ # [Library] SQLite 事务型分布式任务队列
├── tools/
│ └── sync_seeds/ # [Binary] 离线/增量种子同步 CLI 工具
│ └── import_results/ # [Binary] 历史计算结果导入工具(旧版网格结果 → 标记已完成 + 迁移产物树)
└── docs/ # 分主题架构与规格技术文档
```
+53 -40
View File
@@ -9,55 +9,64 @@
```mermaid
erDiagram
WORKFLOWS ||--o{ GRID_POINTS : contains
GRID_POINTS ||--o{ TASK_HISTORY : logs
GRID_POINTS ||--o{ TASKS : logs
NODES ||--o{ TASK_QUEUE : executes
subgraph PrimaryDB ["主数据库 (dcts.db)"]
WORKFLOWS {
string name PK
string description
text yaml_config
text config_yaml
string status
datetime created_at
datetime updated_at
}
GRID_POINTS {
string point_id PK
string name PK
string workflow_name FK
double teff
double logg
double he_abund
double c_abund
double n_abund
double o_abund
double loghe
double logc
double logn
double logo
double cno_sum
integer wave
string status
datetime updated_at
integer attempt_count
string success_method
}
TASK_HISTORY {
string id PK
string point_id FK
TASKS {
string task_id PK
string point_name FK
string node_id
boolean success
text conv_info_json
datetime duration_sec
string task_type
string status
double max_relc
integer retry_count
datetime created_at
}
NODES {
string node_id PK
string hostname
integer cpu_cores
integer max_slots
integer active_slots
string status
double cpu_usage
double memory_usage
datetime last_heartbeat
}
end
}
subgraph QueueDB ["队列库 (dcts_queue.db / mq)"]
TASK_QUEUE {
string task_id PK
string workflow_name
string payload_json
string payload
string status
string assigned_node
integer retry_count
datetime created_at
datetime claimed_at
string workflow_name
string claimed_by_node_id
integer wave
}
end
```
@@ -68,11 +77,11 @@ erDiagram
### 2.1 `workflows` (工作流配置表)
存储用户定义的计算网格配置及整体状态。
- `name` (`VARCHAR(64) PRIMARY KEY`):工作流唯一标志(如 `sdB_cno`)。
- `name` (`TEXT PRIMARY KEY`):工作流唯一标志(如 `sdB_cno`)。
- `description` (`TEXT`):描述信息。
- `yaml_config` (`TEXT`):完整的参数网格定义与物理配置 YAML 内容。
- `status` (`VARCHAR(32)`)状态:`idle` / `running` / `paused` / `completed`
- `created_at` (`DATETIME DEFAULT CURRENT_TIMESTAMP`):创建时间。
- `config_yaml` (`TEXT NOT NULL`):完整的参数网格定义与物理配置 YAML 内容。
- `status` (`TEXT NOT NULL DEFAULT 'idle'`)`idle`(就绪)→ `initializing`(原子抢占加载中)→ `running`(运行中)/ 回退 `idle``running` `paused`(暂停)/ `completed`(全部网格点收敛)
- `created_at` / `updated_at` (`DATETIME NOT NULL`):创建 / 最后更新时间。
### 2.2 `grid_points` (网格点物理参数表)
存储多维笛卡尔积展开后的每一个独立参数点。
@@ -93,26 +102,30 @@ erDiagram
> 标记为 `__legacy__`,不干扰新工作流查询。
### 2.3 `nodes` (计算节点心跳与状态表)
- `node_id` (`VARCHAR(64) PRIMARY KEY`):节点唯一标识。
- `hostname` (`VARCHAR(128)`):节点主机名或 IP
- `cpu_cores` (`INTEGER`):节点 CPU 核心数
- `status` (`VARCHAR(32)`)`online` / `offline` / `busy`
- `last_heartbeat` (`DATETIME`):最后一次心跳上报时间
- `node_id` (`TEXT PRIMARY KEY`):节点唯一标识(未指定 `DCTS_NODE_ID` 时自动生成 `node-<uuid>`
- `max_slots` (`INTEGER NOT NULL`):节点声明的最大并发计算槽位数(即 `DCTS_MAX_SLOTS`,领用上限)
- `active_slots` (`INTEGER NOT NULL DEFAULT 0`):当前正在执行的任务数,随 claim 自增、report 完结自减
- `status` (`VARCHAR(32) NOT NULL DEFAULT 'online'`)`pending_approval`(注册待审批)/ `online`(在线)/ `offline`(心跳超时离线)/ `disabled`(管理员手动停用,保持心跳但不再分发任务)/ `rejected`
- `cpu_usage` / `memory_usage` (`REAL NOT NULL DEFAULT 0.0`):心跳上报的 CPU/内存使用率(仅观测展示用,**不参与任务分发决策**)
- `last_heartbeat` (`DATETIME NOT NULL`):最后一次心跳上报时间,后台线程据此判定 `online → offline`
### 2.4 `task_queue` (分布式任务队列表 - `mq`)
驱动分布式抢占与超时重试的核心表,使用 SQLite `WAL` 模式确保高吞吐并发安全。
- `task_id` (`VARCHAR(128) PRIMARY KEY`):任务 ID。
- `workflow_name` (`VARCHAR(64)`):工作流
- `payload_json` (`TEXT`):任务所含参数 payload
- `status` (`VARCHAR(32)`)`pending`(就绪) / `running`(计算中) / `completed`(完成) / `failed`(失败)。
- `assigned_node` (`VARCHAR(64)`):当前抢占该任务的节点 ID
- `retry_count` (`INTEGER DEFAULT 0`):失败或超时重发次数
- `claimed_at` (`DATETIME`):抢占时间戳(用于超时释放判定)。
驱动分布式抢占与超时重试的核心表,使用 SQLite `WAL` 模式确保高吞吐并发安全(详见 [`sqlite_queue.rs`](file:///home/fmq/program/tlusty/tl208-s54/dcts/crates/mq/src/sqlite_queue.rs#L65)
- `task_id` (`VARCHAR(128) PRIMARY KEY`):任务 IDUUID
- `payload` (`TEXT NOT NULL`):序列化的 [`TaskSpec`](file:///home/fmq/program/tlusty/tl208-s54/dcts/crates/common/src/models.rs)(含 point_name / params / task_type / seed_point_name / workflow_name 等)
- `status` (`TEXT NOT NULL`)`pending`(就绪待领用)/ `claimed`(已被某 node 领用,计算中)
- `created_at` (`DATETIME NOT NULL`):推入队列时间(同 `wave` 内 FIFO 排序键)。
- `claimed_at` (`DATETIME`):被领用的时间戳(`requeue_stale_tasks` 据此判定超时回投)
- `workflow_name` (`TEXT`):所属工作流(多工作流分区键)
- `claimed_by_node_id` (`TEXT`):领用方节点 IDclaim 时写入,report 阶段据此校验归属,防跨节点伪造)。
- `wave` (`INTEGER NOT NULL DEFAULT 0`):难度波次(`pop_task` 出队第一排序键,低 `wave` 优先)。
> 任务完成后由 `remove_task` 直接从本表删除(不保留 `completed`/`failed` 终态行),历史记录落在主库 `tasks` 表。超时的 `claimed` 行由 [`requeue_stale_tasks`](file:///home/fmq/program/tlusty/tl208-s54/dcts/crates/mq/src/sqlite_queue.rs#L274) 改回 `pending` 重投。
---
## 3. 并发与事务安全设计
1. **WAL (Write-Ahead Logging) 模式**SQLite 连接自动启用 `PRAGMA journal_mode=WAL;``PRAGMA busy_timeout=5000;`,解决多线程/多进程读写锁竞争。
1. **WAL (Write-Ahead Logging) 模式**SQLite 连接自动启用 `PRAGMA journal_mode=WAL;``PRAGMA busy_timeout=15000;`,解决多线程/多进程读写锁竞争。
2. **连接池机制**:借助 `r2d2` + `r2d2_sqlite` 维护异步连接池,防止高并发下数据库句柄冲突。
3. **原子 Claim 事务**:任务抢占在单个 SQLite 事务完成`UPDATE task_queue SET status='running', assigned_node=? WHERE status='pending' ... LIMIT 1`),保证绝对防重领
3. **原子 Claim 事务**:任务抢占在单个 `IMMEDIATE` 事务完成——先 `SELECT ... WHERE status='pending' ORDER BY wave ASC, created_at ASC LIMIT 1` 取队首,再 `UPDATE SET status='claimed', claimed_by_node_id=?`,提交后返回。事务保证同一任务不会被两个 node 同时领用;遇到 `SQLITE_BUSY/LOCKED` 最多退避重试 5 次(详见 [`pop_task`](file:///home/fmq/program/tlusty/tl208-s54/dcts/crates/mq/src/sqlite_queue.rs#L145)
+37 -20
View File
@@ -12,7 +12,7 @@ DCTS 计算节点无复杂系统依赖,仅需网络能访问 Master 的 HTTP
[ Master 服务端 ] (拥有固定 IP / 域名, 如 http://192.168.1.100:8090)
├── dcts_server
├── data/dcts.db & data/dcts_queue.db
└── data/results/ 集中种子仓库与计算摘要
└── data/seeds/ 集中种子仓库(node 上报/导入的收敛种子 .7+conv.json,供远程 node 下载热启动)
│ HTTP / REST (8090)
┌─────┴───────────────┬──────────────────────┐
@@ -106,6 +106,7 @@ DCTS 运行时所依赖的物理计算底座二进制文件 `assets/tlusty_stati
```
#### 关键编译选项说明:
- `-fno-automatic`: 禁用局部变量的自动栈分配(强制将局部变量保存在静态内存区)。这是保证传统 FORTRAN 77 程序正常运行的关键参数,防止大型局部数组造成栈溢出(Stack Overflow)或段错误(Segmentation Fault)。
- `-O3`: 开启全量 LLVM/GCC 代码优化,极大加快完全线性化/加速 Lambda 迭代(CL/ALI)及辐射转移方程形式解的计算速度。
- `-mcmodel=large`: 当模型数组与数据段超越 2GB 寻址限制时,允许可执行文件使用 64 位大内存寻址模式。
@@ -118,18 +119,18 @@ DCTS 运行时所依赖的物理计算底座二进制文件 `assets/tlusty_stati
### 3.1 服务端环境变量表 (`server`)
| 环境变量名 | 默认值 | 说明 |
| :---------------------- | :--------------------- | :--------------------------------------------------------------- |
| `DCTS_PORT` | `8090` | 服务端 HTTP REST API 监听端口 |
| `DCTS_DB_PATH` | `data/dcts.db` | 主 SQLite 数据库文件路径(存放节点、网格点及种子记录) |
| `DCTS_QUEUE_DB_PATH` | `data/dcts_queue.db` | 任务队列 SQLite 数据库文件路径 |
| `DCTS_RESULTS_DIR` | `data/results` | 集中种子仓库与计算总结保存目录 |
| `DCTS_BACKUP_DIR` | `data/backups` | 数据库自动/手动备份输出目录 |
| `DCTS_ADMIN_TOKEN` | *空* | 管理员控制台与敏感 API 鉴权令牌 |
| `DCTS_AUTH_TOKEN` | *空* | 旧版全局 API 鉴权令牌(兼容 Admin 与 Enrollment 校验) |
| `DCTS_AUTH_DISABLED` | `false` | 应急开发参数:设置为 `1` 或 `true` 时跳过鉴权 |
| `DCTS_STALE_SEC` | `1800` | 任务运行超时重新放回队列的时间上限(秒) |
| `DCTS_NODE_STALE_SEC` | `120` | 判定 Worker 节点离线的心跳超时时间(秒) |
| 环境变量名 | 默认值 | 说明 |
| :---------------------- | :--------------------- | :----------------------------------------------------- |
| `DCTS_PORT` | `8090` | 服务端 HTTP REST API 监听端口 |
| `DCTS_DB_PATH` | `data/dcts.db` | 主 SQLite 数据库文件路径(存放节点、网格点及种子记录) |
| `DCTS_QUEUE_DB_PATH` | `data/dcts_queue.db` | 任务队列 SQLite 数据库文件路径 |
| `DCTS_SEEDS_DIR` | `data/seeds` | server 端种子库目录(收敛种子 .7+conv.json,供远程 node 下载热启动,**永不清理**) |
| `DCTS_BACKUP_DIR` | `data/backups` | 数据库自动/手动备份输出目录 |
| `DCTS_ADMIN_TOKEN` | *空* | 管理员控制台与敏感 API 鉴权令牌 |
| `DCTS_AUTH_TOKEN` | *空* | 旧版全局令牌(兼容回退为 Admin 凭据,建议迁移到 `DCTS_ADMIN_TOKEN` |
| `DCTS_AUTH_DISABLED` | `false` | 应急开发参数:设置为`1` 或 `true` 时跳过鉴权 |
| `DCTS_STALE_SEC` | `1800` | 任务运行超时重新放回队列的时间上限(秒) |
| `DCTS_NODE_STALE_SEC` | `120` | 判定 Worker 节点离线的心跳超时时间(秒) |
### 3.2 Worker 节点环境变量表 (`node`)
@@ -140,8 +141,16 @@ DCTS 运行时所依赖的物理计算底座二进制文件 `assets/tlusty_stati
| `DCTS_MAX_SLOTS` | `4` | 本地 Worker 节点的并发计算 Slot 槽位数 |
| `DCTS_RUNTIME_DIR` | `data/runtime` | 本地 TLUSTY/SYNSPEC 可执行程序及物理谱线数据存放目录 |
| `DCTS_WORK_DIR` | `data/work` | 本地计算沙盒工作目录 |
| `DCTS_RESULT_DIR` | `data/result` | node 端**完整计算结果归档**目录(光谱/连续谱/各阶段大气快照等全部产物)。超过 200 个网格点子目录时按 LRU 删最旧。旧名 `DCTS_ARCHIVE_DIR` 向后兼容 |
| `DCTS_HEARTBEAT_SEC` | `15` | 向服务端发送心跳报告的时间间隔(秒) |
| `DCTS_AUTH_TOKEN` | *空* | 匹配服务端的 API 鉴权令牌 |
> **两目录分工**(重要,避免混淆):
> - **`data/seeds/`**server 端,`DCTS_SEEDS_DIR`):**种子库**。只存最小集 `conv.json` + `<name>.7`,供 `download_seed` 端点给远程 node 热启动下载。**永不清理**(种子是 SeedStep 必需资源,删除会导致已收敛点重算)。
> - **`data/result/`**node 端,`DCTS_RESULT_DIR`):**完整计算结果归档**。存全部科学产物(`.spec/.cont/.iden/.7/各阶段快照/日志`),供本地留档/排错。LRU 上限 200。
>
> 多机部署下两者物理分离:seeds 在 server 机、result 在各 node 机。单机部署下都挂在 `./data` 下。
> **注**Worker 节点**无需配置任何鉴权令牌**。节点启动后免凭据提交注册申请,由管理员在 Dashboard 审批后自动下发专属 token(持久化到 `runtime/.node_token`)。
---
@@ -190,24 +199,29 @@ DCTS 运行时所依赖的物理计算底座二进制文件 `assets/tlusty_stati
该部署管理框架支持**互动式三步精细向导 (3-Step Wizard)** 与 **自动化长命令行快捷免打扰调度**。
您可以根据具体场景灵活运用以下策略组:
| 安装环境 | 技术引擎 | 适用场景说明 | 推荐自动化直呼执行命令 |
|---|---|---|---|
| **本地 (Local)** | **Docker Compose** | 单机联调或微服务生态整装秒启动 | `./scripts/deploy.sh -e local -b compose -r all` |
| **远程 (Remote)** | **Docker Compose** | 面对严苛依赖的机群打散化打包发布与全动态差分更新 | `./scripts/deploy.sh -e remote -b compose -r all` |
| **本地 (Local)** | **Systemd 原生** | 宿主无虚拟化损耗的高并发物理机运行(具备自启提权判定) | `sudo ./scripts/deploy.sh -e local -b systemd -r all` |
| **远程 (Remote)** | **Systemd 原生** | 主端向分站超算节点无界穿梭远抛落地与托管后台注册 | `./scripts/deploy.sh -e remote -b systemd -r node` |
| 安装环境 | 技术引擎 | 适用场景说明 | 推荐自动化直呼执行命令 |
| ----------------------- | ------------------------ | ------------------------------------------------------ | ------------------------------------------------------- |
| **本地 (Local)** | **Docker Compose** | 单机联调或微服务生态整装秒启动 | `./scripts/deploy.sh -e local -b compose -r all` |
| **远程 (Remote)** | **Docker Compose** | 面对严苛依赖的机群打散化打包发布与全动态差分更新 | `./scripts/deploy.sh -e remote -b compose -r all` |
| **本地 (Local)** | **Systemd 原生** | 宿主无虚拟化损耗的高并发物理机运行(具备自启提权判定) | `sudo ./scripts/deploy.sh -e local -b systemd -r all` |
| **远程 (Remote)** | **Systemd 原生** | 主端向分站超算节点无界穿梭远抛落地与托管后台注册 | `./scripts/deploy.sh -e remote -b systemd -r node` |
#### 1. 交互式多重导航进站直奔体验
在宿主机或者编译主工作区,以最简洁无参数方式执行即唤醒主线指引,全程遵循人性化三层连贯设计选项:
```bash
./scripts/deploy.sh
```
1. **第一步 (环境定位)**:指明需要作用于**本地主机**还是经 SSH 高速管道传输并管理**远端机房控制端**;
2. **第二步 (底层引擎)**:指明借助 **Docker Compose 容器微服务架构**(绝佳无冲突隔离)或是注入原生主机执行的 **Systemd 系统级常驻服务**(也含双端优雅拆毁卸载/一键强停命令分支);
3. **第三步 (目标角色)**:选定**全部服务 [All: Server + Node]**、**仅运维主控服务端 [Server]** 或 **仅挂扣物理分流算力池计算 Worker 节点 [Node]**。
#### 2. 系统服务与集群清收降解管理 (去除与关停)
无论是系统底层的 Systemd 表项或者是持续处于自运行圈范围内部的 Compose 集群容器套,随时均可指派拆除动作清除干净:
```bash
# 卸载或清除对应部署架构,例如卸载本地所有的 systemd 原生守护任务链
./scripts/deploy.sh remove -e local -b systemd -r all
@@ -217,6 +231,7 @@ DCTS 运行时所依赖的物理计算底座二进制文件 `assets/tlusty_stati
```
#### 3. 守护进程实时勘侦管控技巧 (当直接采用 Systemd 环境时)
```bash
# 检查服务端 / Node 计算分核任务运行常态与生存心跳
sudo systemctl status dcts-server
@@ -230,10 +245,12 @@ tail -f data/logs/dcts_node.*.log
### 4.4 方式四:Docker / Docker Compose 标准纯手工控制容器联调(可选方案)
系统由专门精简构筑过的 [Dockerfile.server](file:///home/fmq/program/tlusty/tl208-s54/dcts/Dockerfile.server) 和 [Dockerfile.node](file:///home/fmq/program/tlusty/tl208-s54/dcts/Dockerfile.node) 作为构建基础,默认通过只读载入 `assets` 并以多路复用方式提供极致并发计算生态体系:
```bash
# 快速于本机执行容器冷启聚合与并跑
docker compose up -d --build
```
启动之后访问对口暴露监控 HTTP Dashboard 地址(常规默认指引定位至端口 `8090`),立即获尽实时拓扑状态曲线!
---
+2 -2
View File
@@ -27,8 +27,8 @@
## 2. 节点与网络故障 (Node & Network Issues)
### 现象 1Worker 节点提示 `Unauthorized: Invalid or missing authentication token`
- **原因**Master 服务端启用了 API Auth Token,但 Worker 的配置文件或环境变量中未配置匹配的 `auth_token`
- **解决办法**在 Worker 的 `.env` 中加入 `DCTS_AUTH_TOKEN=<your_secret_token>`
- **原因**该节点的专属 node token 已失效或被重发覆盖(节点鉴权不依赖任何环境变量,而是使用审批时下发的专属 token
- **解决办法**删除节点本地 `runtime/.node_token` 文件并重启节点进程,使其重新免凭据提交注册申请,等待管理员在 Dashboard 审批授权后获取新 token。
### 现象 2:任务长时间处于 `Running` 状态没有进展
- **原因**:Worker 节点在计算中途遭遇断电、内存溢出(OOM)或僵死进程卡死。
+388
View File
@@ -0,0 +1,388 @@
# 工作流执行可视化 —— 功能重设计文档
> 目标:把"恒星大气网格工作流"面板从当前的 **3 个动作(查看 YAML / 启动 / 删除)**
> 升级为完整的 **执行可观测台**:执行进度、逐网格点执行详情、收敛判定、
> 冷启动 vs 种子步进归因、参数空间冷启动成功图谱,以及失败点重试。
>
> 原则:**数据已经在库里大半,缺口主要是 HTTP 暴露 + 少量持久化 + 全新前端 UI。**
> 零新增前端依赖(不引入图表库),沿用现有 ESM + 原生 DOM + 设计令牌风格。
---
## 1. 现状盘点(带代码证据)
### 1.1 已经持久化、可直接用的数据
| 数据 | 位置 | 说明 |
|---|---|---|
| 网格点 6 维参数、`cno_sum``wave` | `grid_points` 表(`db.rs:246-263` | 按 `workflow_name` 分区,复合唯一 `(workflow_name, name)` |
| 点状态 `pending/queued/running/converged/failed` | `grid_points.status` | `GridPointStatus``models.rs:235-243` |
| **成功手段** `cold_run` / `seed_step` / `imported` | `grid_points.success_method` | 收敛时由成功任务的 `task_type` 回填(`db.rs:1097-1101`);失败点为 NULL |
| 重试次数 | `grid_points.attempt_count` | 每次回报 +1`db.rs:1089-1095` |
| 每次尝试的收敛指标 | `tasks.max_relc` | 收敛判据 `max_relc < chmax`(默认 0.001`conv_check.rs:105-109` |
| **每次尝试的种子来源** | `tasks.seed_point_name` | 派发时写入(`scheduler.rs:194-205` |
| 每次尝试的方法 / 错误 / 节点 / 完成时间 | `tasks.task_type / error_message / node_id / completed_at` | 一个点多行(每次尝试一行),工作流删除前长期保留 |
| 全局种子库 | `seeds` 表 + `data/seeds/<name>/<name>.7` | 仅收敛且无 NaN 的点入库(`task.rs:187-207` |
| **逐阶段完整诊断**lte→nc→nl / seed_nc→nl | `data/seeds/<name>/conv.json` | `ModelSummary``models.rs:376-391`):每阶段 `converged / best_max_relc / chmax / elapsed_sec`、总耗时、`synspec_sec`、所用种子。**成功与失败的点都会写**(`task.rs:178-184` |
| 按工作流的统计聚合 | `db.get_grid_summary_stats(Some(wf))` | **已实现且有单测**`db.rs:1582-1639`),但没有任何 HTTP 端点调用它 |
### 1.2 缺口(需要补的)
| # | 缺口 | 影响 | 补法 |
|---|---|---|---|
| G1 | 无逐点 HTTP 端点 | 前端拿不到任何点级数据 | 新增 `/workflows/:name/points` |
| G2 | 无按工作流的统计端点(`get_grid_summary_stats` 未暴露;`/api/status` 只有全局) | 卡片/详情无法显示每工作流进度 | 新增 `/workflows/:name/stats`;列表接口内联计数 |
| G3 | `queued` 被并进 `pending``db.rs:1596,1612` | 无法区分"排队中"与"未入队" | 新端点拆开统计 |
| G4 | 单点耗时/迭代数未进库(`TaskReport.elapsed_sec` 收到即丢;`last_iter``StageSummary` 前被丢弃) | 列表无耗时列、无 ETA | 迁移补列 + runner 携带 `last_iter`Phase 3 |
| G5 | 无进度时间序列 | 画不出"进度-时间"曲线 | 新表 `workflow_progress_snapshots`Phase 3 |
| G6 | 失败点无法重试(start 只重置 `queued``db.rs:886-899`) | 失败点永久卡死 | 新增 retry 端点(复用 `reset_specific_grid_points_to_pending` 思路) |
| G7 | conv.json 不提供下载/查看 | 逐阶段诊断无法展示 | 点详情端点读盘解析返回 |
| G8 | 前端无任何进度/详情 UI;无 tab 组件 | —— | 全新 UI |
### 1.3 领域语义(设计依据)
- **冷启动 vs 种子步进不是用户选的,是调度器按"种子可用性"自动决定的**:
派发时 `find_best_seed_from_db` 命中(同族精确匹配优先,否则全局加权距离 ≤ 3.0,
`seed_finder.rs:13-38`)→ `SeedStep`(热启动链 `seed_nc→nl`),否则 `ColdRun``lte→nc→nl`)。
- **wave = 难度波次**:按 `cno_sum` 升序分组,"先易后难",让早期收敛点充当种子。
所以"哪些参数冷启动能成功" ≈ 低 cno_sum / 中低温区的点;高温 He-poor 区几乎全靠种子步进。
- **还有一次性失败回退**:冷启动发散且存在邻居种子时,自动以 `seed_step` 重投一次
`scheduler.rs:270-350`,受 `seed_step_fallback` 配置与 `has_seed_step_attempt` 一次性闸门约束)。
因此 `success_method='seed_step'` 的点可能是"冷启动失败后被救回"的——该归因需从 `tasks` 多行历史还原。
- **第三类 `imported`**:历史 `run_grid.py` 结果导入(`db.rs:966-981`),UI 需单独呈现为"历史导入"。
- **收敛判据**:末次迭代最差深度点的最大相对修正 `max_relc < chmax`(默认 1e-3),
且大气 NaN 占比 ≤ 10%`conv_check.rs:127-149`)。
---
## 2. 功能设计(信息架构)
### 2.1 工作流卡片(改造现有 `workflows.js` 卡片)
```
┌─────────────────────────────────────────────┐
│ sdB_cno [运行中] │ ← 现有:名称/描述/状态徽章
│ sdB 6维恒星大气模型计算网格 │
│ ▓▓▓▓▓▓▓▓▓▓▓▓░░░░░░░░░░░░░░░ 42% │ ← 新增:收敛进度条 + 百分比
│ 181/432 收敛 · 12 种子步进 · 3 失败 · 8 运行 │ ← 新增:一行计数摘要
│ [进入详情 →] [查看YAML] [暂停] [删除] │ ← 整卡头部可点进详情页
└─────────────────────────────────────────────┘
```
- 卡片名/头部区域是 `<a href="#/workflows/sdB_cno">`;操作按钮 `stopPropagation` 保持独立语义。
- 进度数据来自 **列表接口内联计数**(见 §3.2),不额外轮询。
- 状态徽章扩充:`initializing` 显示"初始化中"warning 琥珀),`paused` 显示"已暂停"secondary),
不再把三者都归为"闲置"。
### 2.2 工作流详情页(独立页面,hash 路由 `#/workflows/:name`
> **信息架构决策**:详情内容量(三 Tab + 可过滤点表 + 热力矩阵 + 逐点诊断)远超模态框承载,
> 且需要深链/返回键/长时间监控驻留。因此采用**独立视图页面** + 零依赖 hash 路由,
> 而非宽模态。入口:卡片整卡或"详情"按钮 → `location.hash = '#/workflows/<name>'`。
**页面骨架**(顶部常驻"任务控制条",是全页最先映入眼帘的部分——直接呈现这个网格的实时生命体征):
```
← 返回控制台 sdB_cno [运行中] [启动/暂停] [重试失败] [查看YAML] [删除]
━━━━━━━━━━━━━━━━━━━━━━━━━━━━▓▓▓▓▓▓░░░░░░░░░░░░░░░░░░░ ← 分段进度条:绿=收敛 红=失败 琥珀=运行 蓝=排队 灰=待定
432 网格点 · 181 收敛(42%) · 8 运行 · 40 排队 · 3 失败 · 冷启动 169 / 种子步进 12 · ETA ≈ 2h40m
```
- 分段进度条按状态占比着色拼接,5s 轮询下各段宽度此消彼长——进度是"看得见在动"的。
- 计数用 `tabular-num` 防跳动;数字变化可加轻量过渡。
- 工作流不存在(hash 指向已删除的名字)→ 空态卡 + 返回按钮。
**Tab 1 — 执行概览**
- 4 个 metric tile(复用 `.metric-card`):网格总数 / 已收敛 / 失败 / 运行中+排队。
- **方法归因**:冷启动收敛 X · 种子步进收敛 Y · 历史导入 Z · 失败 N(彩色计数行)。
- wave(难度波次)分布:每波次 `converged/total` 的小条形列表——直观看到"难度推进到哪了"。
- **最近动态流**:最近 12 条尝试回报(点名 / 方法徽章 / 结果 / max_relc / 节点 / 完成时间),
轮询下新条目从顶部淡入——数据就是 `points` 端点 `sort=last_completed_at&order=desc&limit=12`,零新增端点。
- ETA(Phase 3):按平均单点耗时 × 剩余点数 ÷ 集群空闲槽位估算。
**Tab 2 — 网格点明细**
- 过滤器:状态(全部/排队/运行/收敛/失败)、方法(全部/冷启动/种子步进/导入)、wave 下拉、点名搜索框。
- 表格(复用 `.data-table` + 关键行增量刷新,仿 `nodesTable.js`):
| 列 | 来源 |
|---|---|
| 点名(mono 省略,title 全文) | `name` |
| Teff / logg / logHe / CNO和 | 6 维参数 |
| wave | `wave` |
| 状态 | badge:收敛=online 绿 / 失败=danger 红 / 运行=warning 琥珀 / 排队=info 蓝 / 待定=secondary |
| 方法 | badge:冷启动=蓝(blueprint/ 种子步进=紫(purple/ 导入=secondary——**刻意区别于状态色** |
| max_relc | `tabular-num`,科学计数;失败点显示最后一次尝试值 |
| 尝试数 | `attempt_count`>1 且收敛 = "被救回",加 ↻ 角标) |
| 耗时 | Phase 3 起有值;之前用 `completed_at-created_at` 粗估并标 ~ |
| 操作 | [查看] → 点详情 |
- 分页:`limit=100 + offset`,服务端排序(默认 wave→cno_sum→teff)。
- **刷新策略**:打开弹窗时拉一次;Tab 激活期间每 5s 自动刷新(可手动刷新按钮强制);
弹窗关闭即停。不污染全局轮询。
**Tab 3 — 收敛性分析(冷启动图谱)**
- 核心问题:"哪些参数范围内冷启动能成功"。
- 6 维空间降到 2D 热力矩阵:用户从下拉选 X 轴 / Y 轴(默认 X=Teff、Y=cno_sum);
其余维度若为多值则提供切片选择器(固定某维取值)。
- 单元格着色(CSS grid,无图表库):
- 🟩 冷启动收敛(`converged && method=cold_run`
- 🟪 种子步进收敛(`seed_step`
- ⬜ 历史导入(`imported`
- 🟥 失败(`failed`
- 🟨 运行/排队中
- ░ 未开始(`pending` 且从未尝试)
- 矩阵下方:图例 + 聚合结论条("冷启动成功率 62%268/432);Teff≥60000 区间冷启动成功率 0%,全部由种子步进救回 41 个、失败 3 个")。
- 单元格 hover 显示点名与 max_relc;点击 → 同 Tab 2 的点详情。
### 2.3 网格点详情(右侧滑入面板 slide-over,不再模态套模态)
点表格/热力图/动态流中任一点名 → 右侧滑入 480px 面板(`.point-panel`,背景遮罩 + Esc/点击遮罩关闭,
复用焦点圈管理;独立页面上滑入面板比二级模态更轻、视线不跳转)。
- 头部:点名(mono)+ 状态/方法徽章 + [复制点名]。
- **尝试历史表**`tasks` 多行):# / 方法 / 种子来源(点名,可点击跳查该点)/ max_relc / 结果 / 节点 / 完成时间 / 错误摘要。
这一张表直接还原"冷启动失败 → 种子步进救回"的完整剧情。
- **逐阶段链诊断**(读 `conv.json`):每阶段一张小卡:
`lte` ✅ grey start · `nc` ⚠️ max_relc=0.957(不要求收敛)· `nl` ✅ max_relc=6.9e-3 / 17 iter / 612s
种子步进链则显示 `seed_nc → nl`,并标注种子点名与总耗时、synspec 耗时、NaN 标志。
- 失败点:突出显示最后一次 `error_message``code-pre-wrap`textContent 渲染防 XSS)。
### 2.4 视图与操作逻辑
**路由**(新增 `src/router.js`,约 70 行,零依赖):
| hash | 视图 | 挂载动作 | 卸载动作 |
|---|---|---|---|
| `#/`(或空/未知) | 控制台首页(现有布局) | 渲染骨架 + `startPolling()` | 停全局轮询 |
| `#/workflows/:name` | 工作流详情页 | 渲染页面骨架 + 拉 stats/points + 启作用域轮询 | 停作用域轮询 |
- `window.addEventListener('hashchange', render)`;首次加载按当前 hash 渲染。
- **登录门禁不变**:无 token 时任何 hash 都先展示登录遮罩,登录成功后按 hash 进入对应视图。
- 全局 header/footer 保持常驻;视图内容渲染进 `#view-root` 容器(`index.html` 结构调整:
现有 `main.main-container` 内容收敛为首页视图模板)。
- 返回键/深链/刷新天然可用(hash 不触发整页刷新)。
**交互一览**
| 操作 | 前端 | 后端 |
|---|---|---|
| 进详情页 | hash 切换 → 按需 fetch stats + points(第1页) | —— |
| 切 Tab | 委托 `data-wf-tab`;切到"明细/分析"时惰性加载 | —— |
| 过滤/翻页/排序 | change/click → 重新 fetch(带 query | 端点支持参数 |
| 自动刷新 | 详情页挂载期间 5s 拉 stats(控制条+概览常驻刷新);明细 Tab 激活时同步刷新表格 | —— |
| 点详情面板 | 行内 [查看]/点名链接 → 拉 `points/:point` → 滑入面板;种子来源点名可跳查(换载面板内容) | —— |
| 重试失败点 | `showConfirm` → POST → toast → 刷新 stats/points | 新端点,仅 `running` 状态允许 |
| 返回首页 | "← 返回控制台"链接 / 浏览器返回 | —— |
| 首页卡片 | 整卡点击进详情;卡片上保留 启动/暂停/删除 快捷操作 + 查看YAML(仍为模态) | —— |
---
## 3. 后端设计
### 3.1 新增端点(全部挂在 `/api/workflows/:name/...` 下)
> 鉴权:`required_role` 里 `path.starts_with("/workflows/")` 已映射 Admin`mod.rs:92-95`),
> 子路由**无需改鉴权矩阵**。统一返回 `{success, message, data}` 信封(与现有 workflow API 一致)。
> 点名参数复用现有字符集白名单校验(`task.rs:133-146` 同款),防路径穿越。
#### ① `GET /api/workflows/:name/stats` —— 每工作流进度(G2/G3)
复用并增强 `get_grid_summary_stats(Some(wf))`**拆开 pending/queued**,补方法与波次维度:
```json
{
"success": true, "message": "ok",
"data": {
"name": "sdB_cno", "status": "running",
"total": 432,
"pending": 120, "queued": 40, "running": 8, "converged": 261, "failed": 3,
"cold_run_converged": 220, "seed_step_converged": 41, "imported_converged": 0,
"waves": [ {"wave": 0, "total": 108, "converged": 108, "failed": 0}, ... ],
"avg_point_sec": 740.0, "eta_sec": 9600
}
}
```
- `avg_point_sec` / `eta_sec`Phase 3 有 `elapsed_sec` 列后精确计算;
之前用 `tasks.completed_at - tasks.created_at` 均值近似(含排队等待,偏大,标注近似)。
- SQL 走现有覆盖索引 `idx_grid_points_wf_status`,单次聚合扫描,5s 轮询无压力。
#### ② `GET /api/workflows/:name/points` —— 逐点列表(G1
参数:`status``method``wave``q`(点名子串)、`sort``wave|teff|max_relc|attempts`,默认 wave)、
`order``limit`(≤500,默认 100)、`offset`
```json
{
"success": true, "message": "ok",
"data": {
"total": 432,
"points": [
{
"name": "t60000_g5.0_he-2_c-4_n-4_o-4",
"teff": 60000, "logg": 5.0, "loghe": -2, "logc": -4, "logn": -4, "logo": -4,
"cno_sum": -12, "wave": 0,
"status": "converged", "success_method": "seed_step", "attempt_count": 2,
"last_max_relc": 0.00043, "last_task_type": "seed_step",
"seed_point_name": "t60000_g5.0_he2_c-4_n-4_o-4",
"node_id": "node-a1b2", "last_completed_at": "2026-07-30T11:12:00Z",
"elapsed_sec": 126.4
}
]
}
}
```
- "最近一次尝试"用关联子查询取 `tasks` 最新行:
```sql
SELECT gp.*, t.max_relc AS last_max_relc, t.task_type AS last_task_type,
t.seed_point_name, t.node_id, t.completed_at AS last_completed_at,
t.error_message AS last_error
FROM grid_points gp
LEFT JOIN tasks t ON t.task_id = (
SELECT t2.task_id FROM tasks t2
WHERE t2.point_name = gp.name AND t2.workflow_name = gp.workflow_name
ORDER BY t2.completed_at IS NULL, t2.completed_at DESC, t2.created_at DESC
LIMIT 1)
WHERE gp.workflow_name = ?1 [AND ...filters]
ORDER BY gp.wave, gp.cno_sum, gp.teff
LIMIT ? LIMIT OFFSET ?;
```
- 配套新索引:`CREATE INDEX idx_tasks_point_wf_time ON tasks(point_name, workflow_name, completed_at);`
(迁移中补;432 点规模下无索引也能跑,加索引是为大网格兜底。)
- 备选(若大网格性能不够):把 `last_*` 字段反规范化进 `grid_points`,在 `record_task_report` 里顺手写——
但**默认不做**,避免迁移面扩大;先用 JOIN,压测后再议。
#### ③ `GET /api/workflows/:name/points/:point` —— 单点详情 + 阶段诊断(G7)
```json
{
"success": true, "message": "ok",
"data": {
"point": { /* */ },
"attempts": [
{"task_id": "...", "task_type": "cold_run", "seed_point_name": null,
"status": "failed", "max_relc": 954000.0, "atmosphere_has_nan": true,
"node_id": "node-a1b2", "error_message": "nl stage diverged...",
"created_at": "...", "completed_at": "..."},
{"task_id": "...", "task_type": "seed_step", "seed_point_name": "t60000_...",
"status": "completed", "max_relc": 0.00043, ...}
],
"conv": { /* data/seeds/<name>/conv.json ModelSummary null */ }
}
}
```
- `conv` 读盘路径严格拼为 `seeds_dir/<point>/conv.json`(单层目录,point 先过白名单 +
canonicalize 归属兜底),解析失败/不存在 → `null`(前端降级为"诊断文件不可用")。
**不**把整文件塞进列表接口(体积控制)。
#### ④ `POST /api/workflows/:name/points/retry` —— 失败点重试(G6
请求体:`{"names": ["t80000_..."], "all_failed": true}`(二选一)。
- 前置:工作流必须 `running`(否则 400,提示"请先启动工作流"——避免重置后无调度器消费的僵尸态)。
- 逻辑:新 db 函数 `reset_failed_points(wf, names|all)`
`UPDATE grid_points SET status='pending', success_method=NULL WHERE workflow_name=? AND status='failed' [AND name IN (...)]`
随后立即触发一次 `schedule_pending_tasks()`(不必等 30s tick)。
- 重试后的方法仍由调度器按种子可用性自动决定(大概率 seed_step,因为此时种子池已更丰富)——
这与领域语义一致,前端文案如实说明:"重试的点将由调度器自动选择冷启动或种子步进"。
#### ⑤ 列表接口内联计数(G2,卡片进度条用)
`GET /api/workflows``WorkflowSummary` 追加:
```json
{"name": "...", "status": "...", "description": "...",
"created_at": "...", "updated_at": "...",
"stats": {"total": 432, "converged": 261, "failed": 3, "running": 8,
"cold_run_converged": 220, "seed_step_converged": 41}}
```
- 实现:`list_workflows` 里对 `grid_points` 做一次 `GROUP BY workflow_name` 聚合再 map 合并(**单条 SQL,无 N+1**)。
- 未启动(无网格点)的工作流 `stats: null`,前端不显示进度条。
### 3.2 持久化变更(Phase 3,最小迁移)
> Phase 1/2 **零迁移**——全部基于现有列。以下为 Phase 3 的可选增强:
1. `grid_points` 加列:`elapsed_sec REAL`(最近一次成功/失败尝试的墙钟耗时)。
`record_task_report` 写入——`TaskReport.elapsed_sec` 本来就在手(`models.rs:318`),当前被丢弃。
2. `StageSummary` 携带 `last_iter / worst_depth / n_depths``models.rs:364-373` 扩字段,
`runner.rs:302-317``ConvCheckResult` 已有值,只是没搬过去)→ conv.json 与点详情获得迭代数。
3. 新表 `workflow_progress_snapshots(workflow_name TEXT, ts DATETIME, pending INT, queued INT,
running INT, converged INT, failed INT)`:由 `main.rs:111-197` 的 30s 后台循环顺手写一行;
保留策略:仅当计数相对上次快照有变化才写;定期清理 24h 前的行。供"进度-时间"曲线与精确 ETA。
### 3.3 明确不做 / 保持现状
- **不**改 pull 调度模型、不改 wave 语义、不改种子匹配算法——只读取与展示。
- **不**引入 WebSocket/SSE5s 轮询 + 详情弹窗作用域轮询足够,复杂度与收益不匹配。
- **不**给 `/api/seed/:name` 开 Admin 权限(它是 Node 角色的二进制通道);前端只需 conv.json 的 JSON 内容,走 ③ 即可。
- `seeds` 表/`.7` 文件不暴露列表下载(超出本次范围,且属 Node 域)。
### 3.4 安全清单(对照全局安全规约)
- 所有新端点经现有 `auth_middleware`(Admin 角色)+ 全局限流;无新增鉴权旁路。
- 点名/工作流名全部过字符集白名单(`[A-Za-z0-9._-]`);conv.json 读盘路径不接受 `..`/绝对路径。
- 无新增密钥;错误信息用现有 `AppError`Internal 不回显细节,`error.rs:28-34`)。
- 前端所有服务端字符串经 `escapeHtml`(列表/表格)或 `textContent`conv.json、错误文本)渲染。
- retry 端点为写操作:仅 Admin、受限于 running 状态、参数白名单,无注入面(参数化查询)。
---
## 4. 前端实现设计
### 4.1 新增/改动文件
| 文件 | 改动 |
|---|---|
| `src/router.js`(新) | hash 路由:`parseHash()` → `{view:'home''workflow', name?}``hashchange` 监听 + 首载渲染;视图挂载/卸载生命周期钩子(停/启作用域轮询);登录门禁判断 |
| `src/views/home.js`(新) | 现 `index.html` 中 `main.main-container` 的首页内容收敛为模板函数 + 挂载逻辑(metric 卡/节点表/工作流卡列表),`startPolling()` 在此触发 |
| `src/views/workflowDetail.js`(新) | 详情页:任务控制条(分段进度条/计数/ETA/操作)+ Tab 容器;概览(tiles/方法归因/wave 分布/最近动态流)、点表格(关键行签名 diff,仿 nodesTable)、过滤器、热力矩阵(CSS grid 单元格)、点详情滑入面板。所有插值 `escapeHtml`;作用域 5s 轮询自管 |
| `src/api.js` | 新增 `fetchWorkflowStatsApi(name)`、`fetchWorkflowPointsApi(name, params)`、`fetchPointDetailApi(name, point)`、`retryPointsApi(name, body)`;全部经 `apiFetch` + `encodeURIComponent`,返回 raw Response |
| `src/components/workflows.js` | 卡片加进度条/计数/状态徽章扩充;头部包成进详情的锚链接;读取列表内联 `stats`;空 stats 降级 |
| `src/main.js` | 瘦身为全局接线:登录/主题/登出/全局模态(create-wf / view-yaml / reissue / confirm/ header 刷新;视图级委托移入各 view |
| `src/state.js` | 全局轮询仅服务首页;视图切换时由 router 停启 |
| `index.html` | `main` 收敛为 `<div id="view-root">`(视图由 JS 渲染);保留登录遮罩/header/footer/全局模态/toast;新增 `#point-panel` 滑入面板骨架 |
| `src/style.css` | 新组件:`.wf-status-strip`(控制条 + 分段进度条 `.seg-progress` 五色段)、`.wf-tabs`tab 栏,双主题)、`.method-badge.cold/.seed/.imported`(蓝/紫/中性,**区别于状态色**)、`.heatmap`/`.heatmap-cell`(状态色填充 + hover)、`.stage-chip`(阶段诊断小卡)、`.point-panel`(右侧滑入 + 遮罩)、`.activity-feed`(动态流条目淡入);全部走设计令牌 + `prefers-reduced-motion` 降级 |
### 4.2 必须遵循的现有约定(探查结论)
1. 事件一律 `data-*` 委托,不挂逐按钮监听(`main.js:377-388` 模式)。
2. 服务端字符串进 `innerHTML` 前必过 `escapeHtml`;整块文本用 `textContent`。
3. 模态:静态骨架 + `setupFocusTrap`close 按钮类 `modal-close`(否则 Escape 映射失效,`modal.js:21`)。
4. 破坏性操作先 `showConfirm`,反馈只走 `showToast`,变更后 `fetchAllData()` 立即刷新。
5. 轮询下渲染:行/卡 keyed by `data-*` + 签名 diff + 高频单元格 `textContent` 原位刷新(`nodesTable.js:34-54` 模式),空态写入做 trim 比较防抖。
6. 样式只用令牌;状态复用 `.status-badge` 变体;进度复用 `.progress-bar-track/-fill`;表格复用 `.data-table` + `.col-*` + `.tabular-num`。
7. 工作流标识是 `name`(无数字 id);后端状态含 `initializing`,前端要显式映射。
---
## 5. 分期实施计划
### Phase 1 — 进度可见(MVP,零迁移)
- 后端:`stats` 端点(拆 queued+ `points` 端点(JOIN 最近尝试 + 新索引)+ 列表内联 `stats`。
- 前端:**hash 路由 + 视图化重构** + 卡片进度条/计数/徽章 + 工作流详情页(任务控制条 + 「概览」「网格点明细」两 Tab + 过滤器/分页 + 最近动态流)。
- 验收:深链 `#/workflows/sdB_cno` 打开详情页,能看到 432 点逐行状态、每点方法/max_relc/尝试数/种子来源,进度条随轮询而动。
### Phase 2 — 诊断与归因
- 后端:`points/:point` 端点(尝试历史 + conv.json 解析)+ `retry` 端点 + db `reset_failed_points`。
- 前端:点详情滑入面板(尝试历史 + 阶段链诊断 + 错误)+「收敛性分析」热力 Tab + 「重试失败点」按钮。
- 验收:能回答"这个点为什么失败/被谁救回",能看到冷启动成功参数区,能一键重试失败点。
### Phase 3 — 时序与精算(可选增强)
- 后端:迁移(`elapsed_sec` 列、`StageSummary.last_iter`+ `workflow_progress_snapshots` + ETA 精算。
- 前端:概览 ETA 精确化 + 进度-时间 sparkline + 点表耗时列去"~"。
---
## 6. 待确认的开放问题
1. Phase 范围:三期全做,还是先交付 Phase 1+2?
2. 「重试失败点」是否需要(它会触发真实计算任务)?默认仅 running 时可用。
3. 热力矩阵默认轴 Teff × cno_sum 是否符合你的物理直觉?(sdB_cno 当前网格 logg/logHe/CNO 各只有单值,实际是 Teff 一维——多值网格下矩阵才有意义。)
4. conv.json 之外是否还需要在点详情里提供逐阶段 `.err`/`fort.9` 原始文件下载(目前节点归档 `data/result` 有 LRU 200 上限,服务端只有 conv.json)?
</content>