feat(all): 任务引擎双阶段解耦、僵尸涡旋修复、动态 CPU 配额与前端详情页重构
将 TLUSTY/SYNSPEC 拆为各自独立的 enabled/policy/strategies 阶段,
以策略链自动弹栈取代单级 seed_step 布尔回退;定向修复 2026-08-02
僵尸任务涡旋事故;新增节点并发配额热调;前端详情页从 1412 行巨型
视图拆为薄控制器 + detail 子模块,并补齐工具层与单测。
引擎与调度(task_engine_decoupling_design.md)
- models.rs: 新增 StagePolicy / EngineStageConfig / TaskSpec 阶段字段、
normalize_compat() 校正旧版在途消息策略链、failed_stage 归因
- scheduler.rs: resolve_dispatchable_chain 派发门控、
trigger_strategy_fallback 按 failed_stage 精确弹栈;启动期
force_recompute/skip_converged(默认)/skip_failed 三策略
- db.rs: tasks 表 +7 列持久化阶段配置;终态守卫
(mark_grid_point_running 仅 pending/queued→running;
record_task_report 拒绝迟到失败翻黑 converged);策略弹栈快照
僵尸涡旋修复(runbook-20260802-zombie-vortex-fix.md)
- 全链路跨库活性交叉校验:派发/claim/孤儿回收/回退统一查 MQ 队列活性,
活则放行、死则清僵尸,结构性消除"每点重复派发"
- stop/重启卫生:清队列同步 delete_tasks_by_ids,杜绝遗留 pending 行
- report_task: 幂等吸收 + 409 区分迟到冗余结果,仅 state_changed 时回退
- MQ: NULL workflow_name 回填 __legacy__、requeue 后迟到上报被 403 竞态修复
动态 CPU 配额(dynamic_cpu_slots_design.md)
- admin.rs: POST /admin/nodes/:id/quota(Option<Option<i32>> 区分
缺字段/显式 null);nodes 表 +admin_max_slots
- worker.rs: effective_max_slots = min(admin, physical),心跳下发原子生效
科学产物保全(tlusty_result_artifacts.md)
- runner.rs: SYNSPEC 启动前快照 fort.12/fort.14 → .bfac/.emflux 防覆盖
- 半失败点(大气收敛+光谱失败)改判 Failed 并写入 note;仅 SYNSPEC
场景不再恒判失败;撤销归档 LRU 200 上限改为永久保留
- executor.rs: 透传 synspec_params 数值参数(此前固定 None)
前端(dashboard/)
- workflowDetail.js 1412→328 行,拆出 views/detail/{ctx,overview,
pointsTable,parSets,pointPanel}.js,AbortController 治理监听/请求生命周期
- 删除 wfActions.js,新增 wfEnginePanel.js(双阶段三维配置编辑面板)
- 新增 utils/{errors,format,icons,polling,yamlStage}.js 纯函数模块
- 路由级动态 import 代码分割;节点配额三点菜单 + Modal 管理
- 首次引入 node:test 单测(format/polling/yamlStage/psCache,644 行)
- 系统性补齐 a11y:skip-link、ARIA、Tab 键盘漫游、toast 关闭、退出动画
文档与工具
- 新增 6 篇设计/调研:引擎解耦、动态配额、涡旋 runbook、
光谱正确性分析、收敛判断、产物归档
- PIPELINE/design/api/database 等协同重写为分布式 C/S 架构口径
- scripts/fetch_results.sh 跨节点产物备份;import_results 按 cno 升序导入
- workflows/sdB_cno.yaml: 新增 tlusty/synspec_stage 配置块,修正 wstart 笔误
This commit is contained in:
@@ -0,0 +1,157 @@
|
||||
/* DCTS 格式化与转义工具(权威定义)
|
||||
*
|
||||
* 集中所有纯函数:HTML 转义、时间/时长格式化、状态徽章。
|
||||
* 历史上 escapeHtml 定义在 api.js、各 fmt* 散落在 workflowDetail.js,改一处要全局搜,
|
||||
* 现统一到本模块。api.js 通过 re-export 保持向后兼容。
|
||||
*
|
||||
* SQLite 约定:datetime('now') 形如 'YYYY-MM-DD HH:MM:SS'(UTC,无时区标记),
|
||||
* 解析前需规范化为 ISO。所有时间解析函数共用 parseIso/parseTsMs。
|
||||
*
|
||||
* 语义与后端契约对齐(crates/server/src/db.rs):
|
||||
* - 网格点状态:pending/queued/running/converged/failed/canceled
|
||||
* - success_method(收敛途径):cold_run / seed_step
|
||||
*/
|
||||
|
||||
/**
|
||||
* 安全 HTML 转义,防止存储型 XSS。
|
||||
* 顺序敏感:& 必须先转义,否则后续转义产生的 & 会被二次转义。
|
||||
* @param {unknown} s
|
||||
* @returns {string}
|
||||
*/
|
||||
export function escapeHtml(s) {
|
||||
if (s == null) return '';
|
||||
return String(s)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
/** 规范化为 Date(解析失败返回 null)。SQLite 'YYYY-MM-DD HH:MM:SS' 视为 UTC。 */
|
||||
export function parseIso(iso) {
|
||||
if (!iso) return null;
|
||||
const normalized = iso.includes('T') ? iso : iso.replace(' ', 'T') + 'Z';
|
||||
const d = new Date(normalized);
|
||||
return Number.isNaN(d.getTime()) ? null : d;
|
||||
}
|
||||
|
||||
/** 同 parseIso 但返回 epoch 毫秒(解析失败为 NaN)。供进度曲线按真实时间铺横轴。 */
|
||||
export function parseTsMs(iso) {
|
||||
if (!iso) return NaN;
|
||||
const normalized = iso.includes('T') ? iso : iso.replace(' ', 'T') + 'Z';
|
||||
return new Date(normalized).getTime();
|
||||
}
|
||||
|
||||
/** SQLite datetime('now') 为 'YYYY-MM-DD HH:MM:SS'(UTC 无时区标记),规范化后解析。 */
|
||||
export function fmtTime(iso) {
|
||||
if (!iso) return '—';
|
||||
const normalized = iso.includes('T') ? iso : iso.replace(' ', 'T') + 'Z';
|
||||
const d = new Date(normalized);
|
||||
if (Number.isNaN(d.getTime())) return escapeHtml(iso);
|
||||
return d.toLocaleTimeString('zh-CN');
|
||||
}
|
||||
|
||||
/** 紧凑日期时间「MM-DD HH:MM」:带日期(跨天运行不至于看不出是哪天完成的),
|
||||
* 隐去年份与秒(活跃网格数据不跨年,秒级精度放 title)。定宽 11 字符,列内对齐。 */
|
||||
export function fmtDateTime(iso) {
|
||||
const d = parseIso(iso);
|
||||
if (!d) return iso ? escapeHtml(iso) : '—';
|
||||
const p = (n) => String(n).padStart(2, '0');
|
||||
return `${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`;
|
||||
}
|
||||
|
||||
/** 完整时间戳「YYYY-MM-DD HH:MM:SS」,供单元格 title 悬停查看精确时刻。 */
|
||||
export function fmtFullTs(iso) {
|
||||
const d = parseIso(iso);
|
||||
if (!d) return iso || '';
|
||||
const p = (n) => String(n).padStart(2, '0');
|
||||
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
|
||||
}
|
||||
|
||||
/** 工作流级时长格式化:「N 分钟」或「N 小时 M 分」(控制条 ETA 用)。null/非有限值/≤0 返回占位符。 */
|
||||
export function fmtDuration(sec) {
|
||||
// ≤0 视为无数据(与 fmtDur 同口径):失败任务上报的 last_elapsed_sec 可能是 0.0
|
||||
// (runner 无 summary 时),显示"0 分钟"会误导,应展示占位符。
|
||||
if (sec == null || !Number.isFinite(sec) || Number(sec) <= 0) return '—';
|
||||
const m = Math.round(sec / 60);
|
||||
if (m < 60) return `${m} 分钟`;
|
||||
const h = Math.floor(m / 60);
|
||||
return `${h} 小时 ${m % 60} 分`;
|
||||
}
|
||||
|
||||
/** 网格点级紧凑时长(表格单元格用):42s / 12m 30s / 1h 20m。
|
||||
* ≤0 视为无数据(tlusty 单点不可能 0 秒完成)。 */
|
||||
export function fmtDur(sec) {
|
||||
if (sec == null || sec === '') return '—';
|
||||
const n = Number(sec);
|
||||
if (!Number.isFinite(n) || n <= 0) return '—';
|
||||
const s = Math.round(n);
|
||||
if (s < 60) return `${s}s`;
|
||||
const m = Math.floor(s / 60);
|
||||
if (m < 60) return `${m}m${s % 60 ? ` ${s % 60}s` : ''}`;
|
||||
const h = Math.floor(m / 60);
|
||||
return `${h}h${m % 60 ? ` ${m % 60}m` : ''}`;
|
||||
}
|
||||
|
||||
/** 相对收敛度指数记数(点表/尝试表 max_relc 单元格)。空值/非法值返回占位符。 */
|
||||
export function fmtRelc(v) {
|
||||
if (v == null || v === '') return '—';
|
||||
const n = Number(v);
|
||||
return Number.isFinite(n) ? n.toExponential(2) : '—';
|
||||
}
|
||||
|
||||
/** CSV 字段转义:含逗号/引号/换行时用双引号包裹并翻倍内部引号。 */
|
||||
export function csvEscape(v) {
|
||||
const s = String(v ?? '');
|
||||
return /[",\n\r]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s;
|
||||
}
|
||||
|
||||
/** 心跳时间容错:字段缺失或非法时间戳一律显示占位符,避免渲染出 "Invalid Date"。 */
|
||||
export function fmtClock(ts) {
|
||||
if (!ts) return '—';
|
||||
const d = new Date(ts);
|
||||
if (Number.isNaN(d.getTime())) return '—';
|
||||
return d.toLocaleTimeString('zh-CN');
|
||||
}
|
||||
|
||||
// ===== 状态徽章(返回 HTML 字符串,供 innerHTML 注入) =====
|
||||
|
||||
/** 工作流状态映射:[中文标签, badge class]。后端可发 idle/initializing/running/paused/completed。 */
|
||||
export const STATUS_MAP = {
|
||||
running: ['运行中', 'online'],
|
||||
completed: ['已完成', 'completed'],
|
||||
initializing: ['初始化中', 'warning'],
|
||||
paused: ['已暂停', 'secondary'],
|
||||
idle: ['闲置', 'secondary'],
|
||||
};
|
||||
|
||||
/** 工作流状态徽章 HTML。 */
|
||||
export function statusBadge(status) {
|
||||
const [cn, cls] = STATUS_MAP[status] || ['闲置', 'secondary'];
|
||||
return `<span class="status-badge ${cls}">${cn}</span>`;
|
||||
}
|
||||
|
||||
/** 网格点状态映射:[中文标签, badge class]。与后端 grid_points.status 对齐。 */
|
||||
export const POINT_STATUS_MAP = {
|
||||
converged: ['已收敛', 'online'],
|
||||
failed: ['失败', 'danger'],
|
||||
running: ['运行中', 'warning'],
|
||||
queued: ['排队中', 'info'],
|
||||
pending: ['未开始', 'secondary'],
|
||||
};
|
||||
|
||||
/** 网格点状态徽章 HTML。 */
|
||||
export function pointStatusBadge(status) {
|
||||
const [cn, cls] = POINT_STATUS_MAP[status] || ['未知', 'secondary'];
|
||||
return `<span class="status-badge ${cls}">${cn}</span>`;
|
||||
}
|
||||
|
||||
/** 网格点收敛途径徽章 HTML(success_method:cold_run / seed_step / 策略名)。 */
|
||||
export function pointMethodBadge(method) {
|
||||
if (method === 'cold_run') return '<span class="method-badge cold">冷启动</span>';
|
||||
if (method === 'seed_step') return '<span class="method-badge seed">种子步进</span>';
|
||||
// 其它策略名(如 synspec-only 任务归因的 "standard")原样展示,让光谱归因可见。
|
||||
if (method) return `<span class="method-badge syn">${escapeHtml(method)}</span>`;
|
||||
return '<span class="text-hint">—</span>';
|
||||
}
|
||||
Reference in New Issue
Block a user