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
+97 -32
View File
@@ -10,12 +10,12 @@ export function renderWorkflowsSkeleton() {
<div class="workflow-card skeleton-card">
<div class="workflow-header">
<div>
<div class="skeleton-shimmer skeleton-title" style="width: 140px; height: 18px; margin-bottom: 8px;"></div>
<div class="skeleton-shimmer skeleton-text" style="width: 220px; height: 14px;"></div>
<div class="skeleton-shimmer skeleton-w-140 skeleton-h-18"></div>
<div class="skeleton-shimmer skeleton-w-220 skeleton-h-14"></div>
</div>
<div class="skeleton-shimmer skeleton-badge"></div>
</div>
<div class="workflow-actions" style="margin-top: 16px;">
<div class="workflow-actions wf-actions-mt">
<div class="skeleton-shimmer skeleton-btn"></div>
<div class="skeleton-shimmer skeleton-btn"></div>
</div>
@@ -33,11 +33,23 @@ export function renderWorkflows(workflows) {
if (!workflows || workflows.length === 0) {
if (wfSub) wfSub.textContent = '暂无已注册工作流';
container.innerHTML = `
<div class="empty-cell">
暂无预置工作流。可点击右上角「创建工作流」按钮快速注册。
const emptyHtml = `
<div class="empty-state-box">
<svg class="empty-state-icon" width="36" height="36" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<polygon points="12 2 2 7 12 12 22 7 12 2"/>
<polyline points="2 17 12 22 22 17"/>
<polyline points="2 12 12 17 22 12"/>
</svg>
<p>暂无恒星大气网格工作流配置</p>
<button type="button" class="btn btn-primary btn-sm" data-wf-action="create-first">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
注册第一个工作流
</button>
</div>
`;
if (container.innerHTML.trim() !== emptyHtml.trim()) {
container.innerHTML = emptyHtml;
}
return;
}
@@ -46,23 +58,57 @@ export function renderWorkflows(workflows) {
wfSub.textContent = runningCount > 0 ? `${runningCount} 个工作流运行中` : '集群待命';
}
const html = workflows.map(wf => {
let statusCn = '闲置 (Idle)';
const existingCards = new Map();
container.querySelectorAll('.workflow-card[data-wf-card-name]').forEach(card => {
existingCards.set(card.getAttribute('data-wf-card-name'), card);
});
if (container.querySelector('.skeleton-card, .empty-cell')) {
container.innerHTML = '';
existingCards.clear();
}
const updatedCards = [];
workflows.forEach(wf => {
// 状态徽章全映射:后端可发 idle/initializing/running/paused/completed
let statusCn = '闲置';
let statusClass = 'secondary';
if (wf.status === 'running') {
statusCn = '运行中 (Running)';
statusCn = '运行中';
statusClass = 'online';
} else if (wf.status === 'completed') {
statusCn = '已完成 (Completed)';
statusClass = 'online';
statusCn = '已完成';
statusClass = 'completed';
} else if (wf.status === 'initializing') {
statusCn = '初始化中';
statusClass = 'warning';
} else if (wf.status === 'paused') {
statusCn = '已暂停';
statusClass = 'secondary';
}
const name = escapeHtml(wf.name);
const href = `#/workflows/${encodeURIComponent(wf.name)}`;
const desc = escapeHtml(wf.description || '无描述');
const isRunning = wf.status === 'running';
return `
<div class="workflow-card">
// 内联进度(来自列表接口内联 stats;未启动的工作流 stats=null → 不渲染)
const s = wf.stats;
let progressHtml = '';
if (s && s.total > 0) {
const pct = Math.min(100, Math.max(0, Math.round((s.converged / s.total) * 100)));
progressHtml = `
<div class="wf-card-progress">
<div class="progress-bar-track" aria-hidden="true">
<div class="progress-bar-fill progress-fill-emerald" style="width: ${pct}%"></div>
</div>
<span class="wf-card-counts tabular-num">${s.converged}/${s.total} 收敛 · ${s.seed_step_converged} 种子步进 · ${s.failed} 失败 · ${s.running} 运行</span>
</div>`;
}
const cardInnerHtml = `
<a class="wf-card-link" href="${href}" title="进入工作流详情">
<div class="workflow-header">
<div>
<h3 class="workflow-name">${name}</h3>
@@ -70,26 +116,45 @@ export function renderWorkflows(workflows) {
</div>
<span class="status-badge ${statusClass}">${statusCn}</span>
</div>
<div class="workflow-actions">
<button class="btn btn-secondary btn-sm" data-wf-action="view" data-wf-name="${name}">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>
查看 YAML
</button>
${
isRunning
? `<button class="btn btn-secondary btn-sm" data-wf-action="stop" data-wf-name="${name}"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="6" y="4" width="4" height="16"/><rect x="14" y="4" width="4" height="16"/></svg> 暂停</button>`
: `<button class="btn btn-primary btn-sm" data-wf-action="start" data-wf-name="${name}"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="5 3 19 12 5 21 5 3"/></svg> 启动</button>`
}
<button class="btn btn-danger btn-sm" data-wf-action="delete" data-wf-name="${name}">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
删除
</button>
</div>
${progressHtml}
</a>
<div class="workflow-actions">
<button class="btn btn-secondary btn-sm" data-wf-action="view" data-wf-name="${name}">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><path d="m16 13-2 2 2 2"/><path d="m8 13 2 2-2 2"/></svg>
YAML 配置
</button>
${
isRunning
? `<button class="btn btn-secondary btn-sm" data-wf-action="stop" data-wf-name="${name}"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="6" y="4" width="4" height="16"/><rect x="14" y="4" width="4" height="16"/></svg> 暂停</button>`
: `<button class="btn btn-primary btn-sm" data-wf-action="start" data-wf-name="${name}"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="5 3 19 12 5 21 5 3"/></svg> 启动</button>`
}
<button class="btn btn-danger btn-sm" data-wf-action="delete" data-wf-name="${name}">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
删除
</button>
</div>
`;
}).join('');
if (container.innerHTML.trim() !== html.trim()) {
container.innerHTML = html;
}
let card = existingCards.get(name);
if (card) {
if (card.innerHTML.trim() !== cardInnerHtml.trim()) {
card.innerHTML = cardInnerHtml;
}
existingCards.delete(name);
} else {
card = document.createElement('div');
card.className = 'workflow-card';
card.setAttribute('data-wf-card-name', name);
card.innerHTML = cardInnerHtml;
}
updatedCards.push(card);
});
existingCards.forEach(card => card.remove());
updatedCards.forEach((card, idx) => {
if (container.children[idx] !== card) {
container.insertBefore(card, container.children[idx] || null);
}
});
}