feat(all): NaN 伪收敛否决与 nl_tight 能量回退、emflux 检验 4π 修正、nst 行宽与种子边界修复、gfATO 谱线表接通与网格加密 9216 点、阶段分项统计与跳板机部署

物理修复(400 失败点归因,见 docs/failed400_nan_pseudo_convergence_2026_08_17.md):
- runner: 阶段 converged 后复查 fort.7,含 NaN/Inf 即否决(fort.9 全零伪收敛,
  曾致 261 点误跳过 nl_direct 回退);否决阶段不产出种子,阻断污染传播
- runner: nl_tight 回退——仅能量边际失败时以 CHMAX 收紧 10× 从自身模型续迭代,
  残差降幅 ~10×;execute_tlusty_stage 抽取供主链与回退共用
- conv_check: fort.14 为 Eddington 通量 Hλ,积分需乘 4π 再比 σTeff⁴
  (旧版 ratio 稳定 0.0796=1/4π,全点系统性假阳性)+ 回归测试
- nst_writer: 单行超 80 字符被 TLUSTY 静默截断,IFALI/JALI/TRAD 等从未生效;
  按 75 字符自动换行
- seed_finder: Teff 容忍度改含边界 <=,相邻 5000K 档恢复互为种子 + 回归测试

谱线表与网格:
- 默认线表 gfVIS99 → gfATO(全波段 18-23000Å),TaskSpec.linelist 支持工作流
  级覆盖,节点按需下载(进程互斥锁防并发重复下载 238MB)
- sdB_cno Teff 加密至 5000K 步长,432 → 9216 点;tlusty/synspec 静态二进制更新

统计与部署:
- grid 汇总改按 tlusty_status/synspec_status 分项计数,新增 tlusty_failed/
  synspec_failed/synspec_pending,前端详情页双视图适配
- deploy/fetch_results 支持跳板机 ProxyJump 与 SSH 主连接复用,fetch 新增 --force;
- Docker 构建支持 CARGO_MIRROR/USE_MIRRORS 国内镜像参数;移除 tools/ 拷贝
- 新增 tlusty-synspec-test skill 与 6 篇根因分析/验证文档
This commit is contained in:
fmq
2026-08-17 23:55:26 +08:00
parent 43b82b1ae2
commit b058e66722
54 changed files with 5624 additions and 347 deletions
+8
View File
@@ -67,6 +67,12 @@ const TOKEN_KEY = 'dcts_admin_token';
* @property {number} running
* @property {number} completed
* @property {number} failed
* @property {number} cold_run_converged - TLUSTY 视图:冷启动收敛
* @property {number} seed_step_converged - TLUSTY 视图:种子步进收敛
* @property {number} tlusty_failed - TLUSTY 视图:大气发散
* @property {number} synspec_converged - SYNSPEC 视图:光谱有效
* @property {number} synspec_failed - SYNSPEC 视图:光谱失败
* @property {number} synspec_pending - SYNSPEC 视图:未运行
* @property {number} [eta_sec]
* @property {Array<{label:string,count:number}>} [waves]
*/
@@ -76,6 +82,8 @@ const TOKEN_KEY = 'dcts_admin_token';
* @property {string} name
* @property {string} status
* @property {string} [method]
* @property {string} [tlusty_status] - 'converged'/'failed'/'pending'/null
* @property {string} [synspec_status] - 'converged'/'failed'/'pending'/null
* @property {number} [relc]
* @property {number} [runtime_sec]
* @property {number} [attempts]
+67
View File
@@ -1043,6 +1043,8 @@ body::before {
}
.seg-converged { background: var(--color-success); }
/* 种子步进段(TLUSTY 视图):紫色,与冷启动绿区分收敛策略。 */
.seg-seed { background: var(--accent-purple); }
/* 色盲友好(color-not-only):失败段在红色之上叠加斜线纹理,
即使无法分辨红/绿也能凭纹理识别失败占比。 */
.seg-failed {
@@ -1056,6 +1058,39 @@ body::before {
.seg-queued { background: var(--color-info); }
.seg-pending { background: var(--border-default); }
/* 阶段切换器(TLUSTY/SYNSPEC 双视图)+ 计数行布局 */
.wf-strip-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-3);
flex-wrap: wrap;
}
.phase-switcher {
display: inline-flex;
border: 1px solid var(--border-default);
border-radius: var(--radius-md);
overflow: hidden;
}
.phase-tab {
appearance: none;
border: none;
padding: var(--space-1) var(--space-3);
font-size: var(--font-size-sm);
font-weight: 500;
color: var(--text-secondary);
background: var(--bg-surface);
cursor: pointer;
transition: background 0.15s, color 0.15s;
}
.phase-tab + .phase-tab { border-left: 1px solid var(--border-default); }
.phase-tab:hover { background: var(--bg-subtle); }
.phase-tab.active {
color: var(--color-success);
background: var(--color-success-bg);
font-weight: 600;
}
.wf-strip-counts {
font-size: var(--font-size-sm);
color: var(--text-secondary);
@@ -1155,6 +1190,35 @@ body::before {
border-color: var(--color-danger);
}
/* 阶段状态徽标(单点详情面板顶部):TLUSTY ✓/✗ · SYNSPEC ✓/✗
半失败点显示「整体失败 · TLUSTY ✓ · SYNSPEC ✗」,让阶段级成败一眼可见。 */
.stage-badge {
display: inline-flex;
align-items: center;
padding: 1px var(--space-2);
border-radius: var(--radius-sm);
border: 1px solid transparent;
font-family: var(--font-mono);
font-size: var(--font-size-xs);
font-weight: 500;
white-space: nowrap;
}
.stage-badge.stage-ok {
color: var(--color-success);
background: var(--color-success-bg);
border-color: var(--color-success);
}
.stage-badge.stage-fail {
color: var(--color-danger);
background: var(--color-danger-bg);
border-color: var(--color-danger);
}
.stage-badge.stage-pending {
color: var(--text-muted);
background: var(--bg-subtle);
border-color: var(--border-default);
}
/* 状态徽章补充变体(失败=红 / 排队=蓝) */
.status-badge.danger {
color: var(--color-danger);
@@ -1376,6 +1440,7 @@ body::before {
.ps-cold { fill: var(--color-success); }
.ps-seed { fill: var(--accent-purple); }
.ps-synspec { fill: var(--accent-teal); }
.ps-converged { fill: var(--color-success); }
.ps-failed { fill: var(--color-danger); }
.ps-running { fill: var(--color-warning); }
.ps-queued { fill: var(--color-info); }
@@ -1384,6 +1449,7 @@ body::before {
.ps-seg.ps-cold { fill: var(--color-success); }
.ps-seg.ps-seed { fill: var(--accent-purple); }
.ps-seg.ps-synspec { fill: var(--accent-teal); }
.ps-seg.ps-converged { fill: var(--color-success); }
.ps-seg.ps-failed { fill: var(--color-danger); }
.ps-seg.ps-running { fill: var(--color-warning); }
.ps-seg.ps-queued { fill: var(--color-info); }
@@ -1404,6 +1470,7 @@ body::before {
.ps-vlabel-cold { fill: var(--color-success); font-weight: 600; }
.ps-vlabel-seed { fill: var(--accent-purple); font-weight: 600; }
.ps-vlabel-synspec { fill: var(--accent-teal); font-weight: 600; }
.ps-vlabel-converged { fill: var(--color-success); font-weight: 600; }
.ps-vlabel-failed { fill: var(--color-danger); font-weight: 600; }
.ps-vlabel-running { fill: var(--color-warning); font-weight: 600; }
.ps-vlabel-queued { fill: var(--color-info); font-weight: 600; }
+3
View File
@@ -24,6 +24,9 @@ export function createDetailCtx(name) {
// ===== Tab1 执行概览 =====
activeTab: 'overview',
// 阶段视图切换('tlusty' 大气 / 'synspec' 光谱):概览分段进度条/指标卡 + parSets 状态轴共用。
// 切换按钮在详情页顶部 strip,默认 TLUSTY 视图。
phaseView: 'tlusty',
latestStats: null,
overviewBuilt: false, // 概览骨架仅首次构建,其后轮询原位 diff
lastWavesSig: null,
+114 -35
View File
@@ -14,30 +14,57 @@ import { fetchWorkflowPointsApi, fetchWorkflowProgressApi } from '../../api.js';
import { isAbortError, logError } from '../../utils/errors.js';
import { ICONS } from '../../utils/icons.js';
/** 分段进度条档位:顺序与计数行一致。五段常驻(空档宽度 0),原位改 width 复用 CSS 过渡。 */
const SEG_DEFS = [
['completed', 'seg-converged', '完成'],
['failed', 'seg-failed', '失败'],
/** 分段进度条档位定义——TLUSTY/SYNSPEC 双视图各一套(按 ctx.phaseView 切换)。
* TLUSTY 视图:cold/seed(大气收敛,按策略拆)+ failed(大气发散)+ running/queued/pending。
* SYNSPEC 视图:converged(光谱有效)+ failed(光谱坏)+ pending(未运行)。 */
const SEG_DEFS_TLUSTY = [
['cold', 'seg-converged', '冷启动收敛'],
['seed', 'seg-seed', '种子步进'],
['failed', 'seg-failed', '大气发散'],
['running', 'seg-running', '运行'],
['queued', 'seg-queued', '排队'],
['pending', 'seg-pending', '待定'],
];
const SEG_DEFS_SYNSPEC = [
['converged', 'seg-converged', '光谱有效'],
['failed', 'seg-failed', '光谱失败'],
['pending', 'seg-pending', '未运行'],
];
/** 任务控制条:状态徽章 / 分段进度条 / 计数与 ETA。
*
* 操作按钮(启动/暂停/删除/查看)已移除——全部收敛至内嵌引擎面板
* wfEnginePanel.js,见 docs/task_engine_decoupling_design.md §6)。
* 剩余元素签名 diff 后原位更新,轮询不重建 DOM。 */
export function renderStrip(s) {
/** 选中阶段的档位定义。stats 的字段名映射:cold←cold_run_converged, seed←seed_step_converged,
* failed←(tlusty_failed|synspec_failed), converged←synspec_converged, pending←(pending|synspec_pending)。 */
function segDefsFor(phase) {
return phase === 'synspec' ? SEG_DEFS_SYNSPEC : SEG_DEFS_TLUSTY;
}
/** 把 stats 字段值映射到档位计数。phase 决定 failed/pending 取哪个字段。 */
function segCount(s, key, phase) {
if (key === 'cold') return s.cold_run_converged || 0;
if (key === 'seed') return s.seed_step_converged || 0;
if (key === 'converged') return s.synspec_converged || 0;
if (key === 'failed') return phase === 'synspec' ? (s.synspec_failed || 0) : (s.tlusty_failed || 0);
if (key === 'pending') return phase === 'synspec' ? (s.synspec_pending || 0) : (s.pending || 0);
if (key === 'running') return s.running || 0;
if (key === 'queued') return s.queued || 0;
return 0;
}
/** 任务控制条:状态徽章 / 阶段切换器 / 分段进度条 / 计数与 ETA。
* phase 参数('tlusty'|'synspec')决定分段进度条与计数行用哪套阶段档位。 */
export function renderStrip(s, phase = 'tlusty') {
const statusEl = document.getElementById('wf-detail-status');
if (statusEl && statusEl.getAttribute('data-status') !== s.status) {
statusEl.setAttribute('data-status', s.status);
statusEl.innerHTML = statusBadge(s.status);
}
const SEG_DEFS = segDefsFor(phase);
const segEl = document.getElementById('wf-seg-progress');
if (segEl) {
if (!segEl.firstElementChild) {
// 档位定义随阶段切换变化 → 重建分段(切换阶段时 sig 变化触发重建,同阶段轮询复用)。
const sig = SEG_DEFS.map(d => d[0]).join(',');
if (segEl.getAttribute('data-seg-sig') !== sig) {
segEl.setAttribute('data-seg-sig', sig);
segEl.innerHTML = SEG_DEFS.map(([key, cls, label]) =>
`<div class="seg ${cls}" data-seg="${key}" style="width:0%" title="${label} 0"></div>`).join('');
}
@@ -45,8 +72,7 @@ export function renderStrip(s) {
SEG_DEFS.forEach(([key, , label]) => {
const el = segEl.querySelector(`[data-seg="${key}"]`);
if (!el) return;
const n = s[key] || 0;
// total=0 时 pending 占满整条,沿用旧版「暂无网格点」语义
const n = segCount(s, key, phase);
const w = total > 0 ? (n / total) * 100 : (key === 'pending' ? 100 : 0);
const wStr = `${w}%`;
if (el.style.width !== wStr) el.style.width = wStr;
@@ -57,15 +83,20 @@ export function renderStrip(s) {
const countsEl = document.getElementById('wf-strip-counts');
if (countsEl) {
const pct = s.total ? Math.round((s.completed / s.total) * 100) : 0;
const parts = [
`${s.total} 网格点`,
`${s.completed} 完成${s.total ? ` (${pct}%)` : ''}`,
`${s.running} 运行`,
`${s.queued} 排队`,
`${s.pending} 待定`,
`${s.failed} 失败`,
];
const parts = [`${s.total} 网格点`];
if (phase === 'synspec') {
parts.push(`${s.synspec_converged || 0} 光谱有效`);
parts.push(`${s.synspec_failed || 0} 光谱失败`);
parts.push(`${s.synspec_pending || 0} 运行`);
} else {
const conv = (s.cold_run_converged || 0) + (s.seed_step_converged || 0);
const pct = s.total ? Math.round((conv / s.total) * 100) : 0;
parts.push(`${conv} 大气收敛${s.total ? ` (${pct}%)` : ''}`);
parts.push(`${s.tlusty_failed || 0} 大气发散`);
parts.push(`${s.running} 运行`);
if (s.queued) parts.push(`${s.queued} 排队`);
if (s.pending) parts.push(`${s.pending} 待定`);
}
if (s.eta_sec != null) parts.push(`ETA ≈ ${fmtDuration(s.eta_sec)}`);
const text = parts.join(' · ');
if (countsEl.textContent !== text) countsEl.textContent = text;
@@ -79,12 +110,46 @@ export function renderOverview(ctx, s) {
if (!ctx.overviewBuilt) {
ctx.overviewBuilt = true;
panel.innerHTML = overviewSkeleton();
bindPhaseSwitcher(ctx);
}
updateOverview(ctx, s);
refreshActivityFeed(ctx);
}
/** 概览骨架:动态部分全部挂 ID,供 updateOverview 原位更新。 */
/** 阶段切换器事件绑定:TLUSTY/SYNSPEC 双视图。切换时重渲染 strip + overview + parSets。 */
function bindPhaseSwitcher(ctx) {
document.querySelectorAll('.phase-tab').forEach(btn => {
btn.addEventListener('click', () => {
const phase = btn.getAttribute('data-phase');
if (!phase || phase === ctx.phaseView) return;
ctx.phaseView = phase;
document.querySelectorAll('.phase-tab').forEach(b => {
const active = b.getAttribute('data-phase') === phase;
b.classList.toggle('active', active);
b.setAttribute('aria-selected', String(active));
});
// 重渲染 strip(分段进度条 + 计数行)+ 概览指标卡
if (ctx.latestStats) {
renderStrip(ctx.latestStats, phase);
updateOverview(ctx, ctx.latestStats);
}
// 重渲染 parSets(如果分析 Tab 已有数据)
const container = document.getElementById('ps-container');
if (container && ctx.psState?.points?.length > 0) {
// 触发 parSets 重渲染(renderParSets 在 parSets.js 内部,经 renderAnalysisTab 间接调用)
// 这里直接 import 会循环依赖,用事件委托:parSets 的 renderAnalysisTab 检测 phaseView 变化时重渲染。
// 简化:切到 analysis Tab 时 renderAnalysisTab 会读 ctx.phaseView,已切换则用新档位。
// 若当前已在 analysis Tab,手动触发一次渲染。
if (ctx.activeTab === 'analysis') {
window.dispatchEvent(new CustomEvent('dcts-phase-change', { detail: { phase } }));
}
}
});
});
}
/** 概览骨架:动态部分全部挂 ID,供 updateOverview 原位更新。
* 指标卡按阶段切换语义——"阶段收敛/失败"随 ctx.phaseView 变化。 */
function overviewSkeleton() {
return `
<section class="metrics-grid">
@@ -94,14 +159,14 @@ function overviewSkeleton() {
<span class="metric-sub">6 维参数笛卡尔积展开</span>
</div>
<div class="metric-card card">
<span class="metric-title">已完成</span>
<span class="metric-title" id="ov-m-converged-title">大气收敛</span>
<span class="metric-value tabular-num" id="ov-m-converged">—</span>
<span class="metric-sub" id="ov-m-converged-sub">—</span>
</div>
<div class="metric-card card">
<span class="metric-title">失败</span>
<span class="metric-title" id="ov-m-failed-title">大气发散</span>
<span class="metric-value tabular-num" id="ov-m-failed">—</span>
<span class="metric-sub">冷启动发散且无种子可救</span>
<span class="metric-sub" id="ov-m-failed-sub">—</span>
</div>
<div class="metric-card card">
<span class="metric-title">运行 + 排队</span>
@@ -142,17 +207,31 @@ function setOvText(id, v) {
if (el && el.textContent !== str) el.textContent = str;
}
/** 轮询原位刷新概览数值(指标卡 + 归因徽章 + 波次)。 */
/** 轮询原位刷新概览数值(指标卡 + 波次)。按 ctx.phaseView 切换阶段语义。 */
function updateOverview(ctx, s) {
const pct = s.total ? Math.round((s.completed / s.total) * 100) : 0;
const phase = ctx.phaseView || 'tlusty';
setOvText('ov-m-total', s.total);
setOvText('ov-m-converged', s.completed);
setOvText('ov-m-converged-sub', `完成率 ${pct}%`);
setOvText('ov-m-failed', s.failed);
setOvText('ov-m-runqueue', s.running + s.queued);
setOvText('ov-m-runqueue-sub', `${s.running} 运行 · ${s.queued} 排队`);
// 收敛手段归因面板已移除(见 docs/task_engine_decoupling_design.md §6.1),
// cold/seed 计数仍在点表明细与 parSets 中可用,概览不再单独展示。
setOvText('ov-m-runqueue', (s.running || 0) + (s.queued || 0));
setOvText('ov-m-runqueue-sub', `${s.running || 0} 运行 · ${s.queued || 0} 排队`);
if (phase === 'synspec') {
const conv = s.synspec_converged || 0;
const pct = s.total ? Math.round((conv / s.total) * 100) : 0;
setOvText('ov-m-converged-title', '光谱有效');
setOvText('ov-m-converged', conv);
setOvText('ov-m-converged-sub', `光谱成功率 ${pct}%`);
setOvText('ov-m-failed-title', '光谱失败');
setOvText('ov-m-failed', s.synspec_failed || 0);
setOvText('ov-m-failed-sub', `未运行 ${s.synspec_pending || 0}`);
} else {
const conv = (s.cold_run_converged || 0) + (s.seed_step_converged || 0);
const pct = s.total ? Math.round((conv / s.total) * 100) : 0;
setOvText('ov-m-converged-title', '大气收敛');
setOvText('ov-m-converged', conv);
setOvText('ov-m-converged-sub', `冷启动 ${s.cold_run_converged || 0} · 种子步进 ${s.seed_step_converged || 0}${pct}%`);
setOvText('ov-m-failed-title', '大气发散');
setOvText('ov-m-failed', s.tlusty_failed || 0);
setOvText('ov-m-failed-sub', '收敛阈值未达标且无种子可救');
}
updateWaves(ctx, s);
}
+71 -30
View File
@@ -58,30 +58,45 @@ export function restorePsCache(ctx) {
}
}
/** 状态轴档位(收敛拆冷启动/种子步进/SYNSPEC,物理意义不同:稳定区 vs 救回区 vs 光谱重算)
/** 状态轴档位定义——TLUSTY/SYNSPEC 双视图各一套
* 顺序即图例/轴布局/每值内分段/色带绘制层叠的唯一来源,语义约定:
* 已收敛(最受关注)置顶 → 进行中按任务进展方向(running→queued→pending)→ 失败沉底。
* 色带绘制按此顺序:收敛色带先画(底层),失败最后画(最上层,数量少且颜色醒目)。
* synspec 档:synspec_success_method 非空(如 synspec-only 任务的光谱策略 "standard"),
* 属已收敛但由光谱合成阶段产出,需与大气收敛区分。 */
const PS_STATUS = ['cold', 'seed', 'synspec', 'running', 'queued', 'pending', 'failed'];
*
* TLUSTY 视图(大气阶段):基于 tlusty_status + tlusty_success_method。
* cold/seed = 大气收敛(按策略拆),failed = 大气发散,running/queued/pending = 未跑完。
* SYNSPEC 视图(光谱阶段):基于 synspec_status。
* converged = 光谱有效,failed = 光谱坏(Balmer edge NaN 等),pending = 未运行(多因 tlusty 未收敛阻塞)。 */
const PS_STATUS_TLUSTY = ['cold', 'seed', 'failed', 'running', 'queued', 'pending'];
const PS_STATUS_SYNSPEC = ['converged', 'failed', 'pending'];
const PS_STATUS_LABEL = {
cold: '冷启动收敛', seed: '种子步进', synspec: 'SYNSPEC 合成', failed: '失败',
cold: '冷启动收敛', seed: '种子步进', failed: '失败',
running: '运行', queued: '排队', pending: '未开始',
converged: '光谱有效',
};
/** 点 → 状态档位 key。
* 阶段归因列拆分(P9)后可直接区分:TLUSTY 策略看 tlusty_success_method
* SYNSPEC-only 点看 synspec_success_method——不再需要猜策略名(原 SYNSPEC_METHODS hack)。 */
function psSlot(p) {
if (p.status === 'completed') {
if (p.tlusty_success_method === 'seed_step') return 'seed';
if (p.tlusty_success_method === 'cold_run') return 'cold';
// 光谱阶段收敛(synspec-only/双阶段光谱归因落库)→ SYNSPEC 合成档
if (p.synspec_success_method) return 'synspec';
return 'cold';
/** 当前选中阶段的档位列表(renderParSets 读 ctx.phaseView 切换)。 */
function psStatusList(phase) {
return phase === 'synspec' ? PS_STATUS_SYNSPEC : PS_STATUS_TLUSTY;
}
/** 点 → 状态档位 key(按选中阶段映射)。
* TLUSTY 阶段:tlusty_status='converged' 按 tlusty_success_method 拆 cold/seed'failed'→failed
* 否则按 grid_points.status 归 running/queued/pending
* SYNSPEC 阶段:synspec_status='converged'→converged'failed'→failed;其余→pending(含 NULL)。 */
function psSlot(p, phase) {
if (phase === 'synspec') {
const s = p.synspec_status;
if (s === 'converged') return 'converged';
if (s === 'failed') return 'failed';
return 'pending'; // pending / NULL / 未知 → 未运行
}
if (p.status === 'failed') return 'failed';
// TLUSTY 视图
if (p.tlusty_status === 'converged') {
if (p.tlusty_success_method === 'seed_step') return 'seed';
return 'cold'; // cold_run 或缺省归冷启动
}
if (p.tlusty_status === 'failed') return 'failed';
if (p.status === 'running') return 'running';
if (p.status === 'queued') return 'queued';
return 'pending';
@@ -96,9 +111,29 @@ function psFmtDim(dim, v) {
return n.toFixed(0); // loghe/logc/logn/logo 都是整数采样
}
/** 卸载时清理 parSets 模块注册的 window 级监听器,防止页面离开后旧 ctx 闭包驻留内存
* 并对游离 DOM 执行无效重绘(GC Leak)。由 workflowDetail.unmountWorkflowDetail 调用。 */
export function cleanupParSetsListeners(ctx) {
if (ctx?._psPhaseListener) {
window.removeEventListener('dcts-phase-change', ctx._psPhaseListener);
ctx._psPhaseListener = null;
}
}
export function renderAnalysisTab(ctx) {
const el = document.getElementById('wf-tab-analysis');
if (!el) return;
// 阶段切换器事件:概览页的 TLUSTY/SYNSPEC 切换会派发 dcts-phase-change
// 若分析 Tab 已构建且已有数据,按新阶段重渲染平行集合图。
if (!ctx._psPhaseListener) {
ctx._psPhaseListener = (e) => {
if (ctx.psBuilt && ctx.psState?.points?.length > 0) {
renderParSets(ctx);
renderPsConclusion(ctx, ctx.psState.points);
}
};
window.addEventListener('dcts-phase-change', ctx._psPhaseListener);
}
if (!ctx.psBuilt) {
ctx.psBuilt = true;
el.innerHTML = `
@@ -175,8 +210,10 @@ function renderParSets(ctx) {
return;
}
const phase = ctx.phaseView || 'tlusty';
const PS_STATUS = psStatusList(phase);
const dims = [...PS_DIMS, 'status'];
// 每维取值列表(status 用 PS_STATUS 顺序)
// 每维取值列表(status 用当前阶段的 PS_STATUS 顺序)
const dimValues = {};
PS_DIMS.forEach(d => { dimValues[d] = psDimValues(points, d); });
dimValues.status = PS_STATUS;
@@ -190,7 +227,7 @@ function renderParSets(ctx) {
PS_DIMS.forEach(d => { ptNum[d] = new Float64Array(nPts); });
for (let i = 0; i < nPts; i++) {
const p = points[i];
ptSlotArr[i] = psSlot(p);
ptSlotArr[i] = psSlot(p, phase);
for (const d of PS_DIMS) ptNum[d][i] = Number(p[d]);
}
@@ -365,16 +402,17 @@ export function reflowParSets(ctx) {
if (c && Math.abs((c.clientWidth || 0) - prev) > 20) renderParSets(ctx);
}
/** 自动洞察:扫各维度取值,挑失败率显著高于均值的位置。 */
/** 自动洞察:扫各维度取值,挑失败率显著高于均值的位置。按选中阶段用对应档位计数。 */
function renderPsConclusion(ctx, pts) {
const el = document.getElementById('ps-conclusion');
if (!el) return;
const total = pts.length;
if (total === 0) { el.textContent = ''; return; }
const cold = pts.filter(p => p.status === 'completed' && p.tlusty_success_method === 'cold_run').length;
const seed = pts.filter(p => p.status === 'completed' && p.tlusty_success_method === 'seed_step').length;
const synspec = pts.filter(p => psSlot(p) === 'synspec').length;
const failed = pts.filter(p => p.status === 'failed').length;
const phase = ctx.phaseView || 'tlusty';
// 按当前阶段统计各档位计数(与 psSlot 同口径)
const slotCount = {};
pts.forEach(p => { const s = psSlot(p, phase); slotCount[s] = (slotCount[s] || 0) + 1; });
const failed = slotCount.failed || 0;
const baseFail = failed / total;
const findings = [];
@@ -388,17 +426,18 @@ function renderPsConclusion(ctx, pts) {
});
[...groups.entries()].forEach(([v, g]) => {
if (g.length < 3) return;
const gf = g.filter(p => p.status === 'failed').length / g.length;
const gf = g.filter(p => psSlot(p, phase) === 'failed').length / g.length;
if (gf >= 0.3 && gf - baseFail >= 0.15) {
findings.push({ score: gf - baseFail, text: `${d}=${psFmtDim(d, v)}:失败率 ${Math.round(gf * 100)}%${g.length} 点,均值 ${Math.round(baseFail * 100)}%` });
}
});
});
findings.sort((a, b) => b.score - a.score);
const lines = [
`冷启动 ${cold} · 种子步进 ${seed} · SYNSPEC ${synspec} · 失败 ${failed} / 共 ${total}`,
...findings.slice(0, 3).map(f => f.text),
];
// 结论行按阶段拼装:TLUSTY 视图拆 cold/seed/failedSYNSPEC 视图拆 converged/failed/pending。
const summary = phase === 'synspec'
? `光谱有效 ${slotCount.converged || 0} · 光谱失败 ${slotCount.failed || 0} · 未运行 ${slotCount.pending || 0} / 共 ${total}`
: `冷启动 ${slotCount.cold || 0} · 种子步进 ${slotCount.seed || 0} · 失败 ${slotCount.failed || 0} / 共 ${total}`;
const lines = [summary, ...findings.slice(0, 3).map(f => f.text)];
el.innerHTML = lines.map(l => `<span class="ps-concl-line">${escapeHtml(l)}</span>`).join('');
// 屏幕阅读器文本摘要(screen-reader-summary):平行集合图是 SVG role=img
@@ -460,8 +499,9 @@ function bindPsHover(ctx) {
const dim = seg.getAttribute('data-dim');
const val = seg.getAttribute('data-val');
const slot = seg.getAttribute('data-slot');
const phase = ctx.phaseView || 'tlusty';
const names = ctx.psState.points
.filter(p => psSlot(p) === slot && (dim === 'status' || Number(p[dim]) === Number(val)))
.filter(p => psSlot(p, phase) === slot && (dim === 'status' || Number(p[dim]) === Number(val)))
.map(p => p.name).slice(0, 200);
if (names.length === 0) return;
if (names.length === 1) { openPointPanel(ctx, names[0]); return; }
@@ -476,8 +516,9 @@ function bindPsHover(ctx) {
const dim = seg.getAttribute('data-dim');
const val = seg.getAttribute('data-val');
const slot = seg.getAttribute('data-slot');
const phase = ctx.phaseView || 'tlusty';
const names = ctx.psState.points
.filter(p => psSlot(p) === slot && (dim === 'status' || Number(p[dim]) === Number(val)))
.filter(p => psSlot(p, phase) === slot && (dim === 'status' || Number(p[dim]) === Number(val)))
.map(p => p.name).slice(0, 200);
if (names.length === 0) return;
if (names.length === 1) { openPointPanel(ctx, names[0]); return; }
+16 -1
View File
@@ -88,7 +88,7 @@ export async function openPointPanel(ctx, pointName) {
const json = await res.json();
if (!json.success || !json.data) throw new Error(json.message || '无数据');
const { point, attempts, conv } = json.data;
badges.innerHTML = `${pointStatusBadge(point.status)} ${pointMethodBadge(overallMethod(point))}`;
badges.innerHTML = `${pointStatusBadge(point.status)} ${pointMethodBadge(overallMethod(point))} ${stageBadges(point)}`;
body.innerHTML = '';
body.appendChild(buildAttemptSection(attempts));
body.appendChild(buildConvSection(conv));
@@ -106,6 +106,21 @@ function attemptStageBadge(stage) {
return `<span class="method-badge ${cls}">${label}</span>`;
}
/** 阶段状态双徽标(TLUSTY ✓/✗ · SYNSPEC ✓/✗):从 DB 阶段列读 tlusty_status/synspec_status
* 让半失败点(大气收敛+光谱失败)一打开就能看到「整体失败 · TLUSTY ✓ · SYNSPEC ✗」。
* 旧数据无阶段列(NULL)→ 不渲染(保持向后兼容)。 */
function stageBadges(point) {
const mk = (label, status) => {
if (status === 'converged') return `<span class="stage-badge stage-ok">${label} ✓</span>`;
if (status === 'failed') return `<span class="stage-badge stage-fail">${label} ✗</span>`;
if (status === 'pending') return `<span class="stage-badge stage-pending">${label} …</span>`;
return ''; // NULL → 不渲染
};
const t = mk('TLUSTY', point.tlusty_status);
const s = mk('SYNSPEC', point.synspec_status);
return (t || s) ? `${t} ${s}`.trim() : '';
}
/**
* 从 summary_jsonModelSummary)推导尝试方法徽标(Phase 6 起无 task_type 字段)。
* ModelSummary.seed 存在 → 种子步进热启动;否则冷启动。synspec-only 任务无 seed →
+10 -3
View File
@@ -22,7 +22,7 @@ import {
renderStrip, renderOverview, renderRateLine, renderSparkline, maybeRefreshProgress,
} from './detail/overview.js';
import { renderPointsTab, refreshPoints, exportPointsCsv, updateSortIndicators } from './detail/pointsTable.js';
import { renderAnalysisTab, refreshPsData, closePsPointList, reflowParSets } from './detail/parSets.js';
import { renderAnalysisTab, refreshPsData, closePsPointList, reflowParSets, cleanupParSetsListeners } from './detail/parSets.js';
import { openPointPanel, closePointPanel } from './detail/pointPanel.js';
import { mountEnginePanel, unmountEnginePanel, refreshEnginePanel } from '../components/wfEnginePanel.js';
@@ -63,6 +63,7 @@ export function unmountWorkflowDetail() {
ctx.psTooltipEl.remove();
ctx.psTooltipEl = null;
}
cleanupParSetsListeners(ctx);
ctx.abortCtl.abort();
ctx = null;
}
@@ -122,7 +123,7 @@ async function refreshStats(name) {
const json = await res.json();
if (json.success && json.data) {
ctx.latestStats = json.data;
renderStrip(json.data);
renderStrip(json.data, ctx.phaseView);
if (ctx.activeTab === 'overview') {
renderOverview(ctx, json.data);
// 面板不再每 tick 重建、曲线 DOM 保留:只需把速率行/ETA/停滞横幅与最新统计同步
@@ -156,7 +157,13 @@ function pageSkeleton(name) {
</div>
</div>
<div id="wf-seg-progress" class="seg-progress" aria-hidden="true"></div>
<div id="wf-strip-counts" class="wf-strip-counts tabular-num">正在获取执行数据…</div>
<div class="wf-strip-row">
<div class="phase-switcher" role="tablist" aria-label="阶段视图切换">
<button type="button" class="phase-tab active" data-phase="tlusty" role="tab" aria-selected="true">TLUSTY(大气)</button>
<button type="button" class="phase-tab" data-phase="synspec" role="tab" aria-selected="false">SYNSPEC(光谱)</button>
</div>
<div id="wf-strip-counts" class="wf-strip-counts tabular-num">正在获取执行数据…</div>
</div>
</section>
<!-- 内嵌执行引擎配置面板(启动/停止/删除/保存 + TLUSTY/SYNSPEC 阶段三维配置) -->