2026-10-17 17:15:16 +00:00
<!DOCTYPE html>
< html lang = "zh-CN" >
< head >
< meta charset = "utf-8" >
< meta name = "viewport" content = "width=device-width, initial-scale=1" >
2026-10-18 06:23:31 +08:00
< title > Transformer 多峰策略端到端训练与浏览器部署 · 训练监控</ title >
2026-10-17 17:15:16 +00:00
< style >
: root {
--bg : #0b0f17 ; --bg2 : #0f1521 ; --panel : #131a27 ; --panel2 : #18212f ; --line : #222c3c ; --line2 : #2c3850 ;
--text : #e8edf5 ; --dim : #8c99ad ; --dim2 : #5d6a7e ;
--accent : #5aa9ff ; --accent2 : #7c6cff ; --ok : #34d399 ; --warn : #fbbf24 ; --bad : #f87171 ;
--c1 : #5aa9ff ; --c2 : #fbbf24 ; --c3 : #34d399 ; --c4 : #f87171 ; --c5 : #a78bfa ; --c6 : #22d3ee ; --c7 : #fb923c ;
--mono : "JetBrains Mono" , ui-monospace , SFMono-Regular , Menlo , Consolas , monospace ;
--r : 14 px ;
}
* { box-sizing : border-box ; }
html , body { margin : 0 ; }
body { background : radial-gradient ( 1200 px 600 px at 10 % -10 % , #16223a 0 % , transparent 60 % ), radial-gradient ( 900 px 500 px at 100 % 0 % , #1c1838 0 % , transparent 55 % ), var ( -- bg );
background-attachment : fixed ; color : var ( -- text ); font : 14 px / 1.5 - apple-system , "Segoe UI" , "PingFang SC" , "Microsoft YaHei" , sans-serif ; min-height : 100 vh ; }
/* ---------- 顶栏 ---------- */
header { position : sticky ; top : 0 ; z-index : 20 ; display : flex ; align-items : center ; gap : 14 px ; flex-wrap : wrap ; padding : 12 px 24 px ;
background : rgba ( 11 , 15 , 23 , .78 ); backdrop-filter : blur ( 12 px ); border-bottom : 1 px solid var ( -- line ); }
. brand { display : flex ; align-items : center ; gap : 10 px ; }
. logo { width : 30 px ; height : 30 px ; border-radius : 9 px ; background : linear-gradient ( 135 deg , var ( -- accent ), var ( -- accent2 )); display : grid ; place-items : center ; box-shadow : 0 4 px 18 px #5aa9ff 44 ; }
. logo svg { width : 18 px ; height : 18 px ; }
. brand h1 { font-size : 15 px ; margin : 0 ; font-weight : 650 ; letter-spacing : .3 px ; }
. brand . sub { font-size : 12 px ; color : var ( -- dim ); margin-top : -2 px ; }
header . sp { flex : 1 ; }
. ctl { display : flex ; align-items : center ; gap : 8 px ; color : var ( -- dim ); font-size : 13 px ; }
select , button { background : var ( -- panel2 ); color : var ( -- text ); border : 1 px solid var ( -- line2 ); border-radius : 9 px ; padding : 6 px 10 px ; font : inherit ; font-size : 13 px ; outline : none ; }
select : hover , button : hover { border-color : #3d4c68 ; }
button { cursor : pointer ; display : inline-flex ; align-items : center ; gap : 6 px ; }
button . on { border-color : var ( -- accent ); color : var ( -- accent ); background : #5aa9ff 14 ; }
input [ type = range ] { accent-color : var ( -- accent ); width : 90 px ; }
. pill { display : inline-flex ; align-items : center ; gap : 7 px ; padding : 4 px 11 px ; border-radius : 99 px ; font-size : 12 px ; border : 1 px solid var ( -- line2 ); color : var ( -- dim ); background : var ( -- panel ); }
. pill . dot { width : 7 px ; height : 7 px ; border-radius : 50 % ; background : var ( -- dim2 ); }
. pill . live { color : var ( -- ok ); border-color : #34d399 55 ; } . pill . live . dot { background : var ( -- ok ); box-shadow : 0 0 0 0 #34d399 aa ; animation : pulse 1.6 s infinite ; }
. pill . stale { color : var ( -- warn ); border-color : #fbbf24 55 ; } . pill . stale . dot { background : var ( -- warn ); }
. pill . dead { color : var ( -- bad ); border-color : #f87171 55 ; } . pill . dead . dot { background : var ( -- bad ); }
. pill . demo { color : var ( -- c5 ); border-color : #a78bfa 55 ; } . pill . demo . dot { background : var ( -- c5 ); }
@ keyframes pulse { 0 % { box-shadow : 0 0 0 0 #34d399 99 ; } 70 % { box-shadow : 0 0 0 7 px #34d399 00 ; } 100 % { box-shadow : 0 0 0 0 #34d399 00 ; } }
main { padding : 20 px 24 px 48 px ; max-width : 1680 px ; margin : 0 auto ; display : flex ; flex-direction : column ; gap : 16 px ; }
. card { background : linear-gradient ( 180 deg , #151d2b 0 % , #121926 100 % ); border : 1 px solid var ( -- line ); border-radius : var ( -- r ); padding : 16 px 18 px ; min-width : 0 ; box-shadow : 0 1 px 0 #ffffff 05 inset , 0 8 px 24 px #000000 30 ; }
. card h2 { font-size : 12.5 px ; font-weight : 600 ; color : var ( -- dim ); margin : 0 0 12 px ; display : flex ; justify-content : space-between ; align-items : center ; letter-spacing : .4 px ; }
. card h2 . r { font-weight : 400 ; color : var ( -- dim2 ); font-size : 12 px ; letter-spacing : 0 ; }
/* ---------- 流水线阶段 ---------- */
. steps { display : flex ; gap : 0 ; overflow-x : auto ; padding : 4 px 2 px 2 px ; scrollbar-width : thin ; }
. step { flex : 1 0 96 px ; display : flex ; flex-direction : column ; align-items : center ; gap : 6 px ; position : relative ; min-width : 96 px ; }
. step :: before { content : "" ; position : absolute ; top : 14 px ; left : -50 % ; width : 100 % ; height : 2 px ; background : var ( -- line2 ); z-index : 0 ; }
. step : first-child :: before { display : none ; }
. step . done :: before , . step . active :: before { background : linear-gradient ( 90 deg , var ( -- ok ), var ( -- ok )); }
. step . ic { width : 28 px ; height : 28 px ; border-radius : 50 % ; display : grid ; place-items : center ; font : 600 12 px var ( -- mono ); background : var ( -- panel2 ); border : 2 px solid var ( -- line2 ); color : var ( -- dim ); z-index : 1 ; }
. step . done . ic { background : #34d399 22 ; border-color : var ( -- ok ); color : var ( -- ok ); }
. step . active . ic { background : #5aa9ff 22 ; border-color : var ( -- accent ); color : var ( -- accent ); box-shadow : 0 0 0 4 px #5aa9ff 1 f ; }
. step . fail . ic { background : #f87171 22 ; border-color : var ( -- bad ); color : var ( -- bad ); }
. step . nm { font-size : 12.5 px ; color : var ( -- text ); white-space : nowrap ; } . step . ds { font-size : 11 px ; color : var ( -- dim2 ); white-space : nowrap ; }
. step . pending . nm { color : var ( -- dim ); }
. step . active . ds { color : var ( -- accent ); }
/* ---------- 进度主卡 + GPU ---------- */
. hero { display : grid ; grid-template-columns : minmax ( 0 , 2.2 fr ) minmax ( 300 px , 1 fr ); gap : 16 px ; }
. prog-top { display : flex ; align-items : flex-end ; justify-content : space-between ; gap : 16 px ; flex-wrap : wrap ; }
. prog-top . ttl { font-size : 13 px ; color : var ( -- dim ); } . prog-top . ttl b { color : var ( -- text ); font-weight : 600 ; font-size : 16 px ; margin-right : 8 px ; }
. tag { display : inline-block ; font-size : 11 px ; padding : 1 px 8 px ; border-radius : 6 px ; background : #5aa9ff 1 c ; color : var ( -- accent ); border : 1 px solid #5aa9ff 33 ; vertical-align : 2 px ; }
. tag . apg { background : #a78bfa 1 c ; color : var ( -- c5 ); border-color : #a78bfa 33 ; } . tag . bench { background : #fbbf24 1 c ; color : var ( -- warn ); border-color : #fbbf24 33 ; }
. bigpct { font : 700 44 px / 1 var ( -- mono ); letter-spacing : -1 px ; background : linear-gradient ( 90 deg , #fff , #b9d7ff ); -webkit- background-clip : text ; background-clip : text ; color : transparent ; }
. bigpct small { font-size : 18 px ; margin-left : 2 px ; }
. bar { position : relative ; height : 12 px ; background : #0d131d ; border : 1 px solid var ( -- line ); border-radius : 99 px ; overflow : visible ; margin : 16 px 0 10 px ; }
. bar . fill { height : 100 % ; border-radius : 99 px ; background : linear-gradient ( 90 deg , var ( -- accent ), var ( -- c6 )); box-shadow : 0 0 14 px #5aa9ff 66 ; transition : width .8 s ease ; position : relative ; }
. bar . fill :: after { content : "" ; position : absolute ; inset : 0 ; border-radius : 99 px ; background : repeating-linear-gradient ( 45 deg , #ffffff 14 0 8 px , transparent 8 px 16 px ); animation : move 1.2 s linear infinite ; }
. bar . idle . fill :: after { animation : none ; display : none ; }
@ keyframes move { from { background-position : 0 0 ; } to { background-position : 22 px 0 ; } }
. bar . mk { position : absolute ; top : -4 px ; width : 2 px ; height : 20 px ; border-radius : 1 px ; background : #ffffff 30 ; }
. bar . mk . best { background : var ( -- ok ); }
. facts { display : grid ; grid-template-columns : repeat ( auto - fit , minmax ( 130 px , 1 fr )); gap : 10 px ; margin-top : 14 px ; }
. fact { background : #0f1520 ; border : 1 px solid var ( -- line ); border-radius : 10 px ; padding : 9 px 12 px ; }
. fact . k { font-size : 11.5 px ; color : var ( -- dim ); } . fact . v { font : 600 15 px / 1.35 var ( -- mono ); margin-top : 2 px ; white-space : nowrap ; }
. gpuwrap { display : flex ; gap : 16 px ; align-items : center ; }
. ring { position : relative ; width : 116 px ; height : 116 px ; flex : none ; }
. ring svg { transform : rotate ( -90 deg ); }
. ring . c { position : absolute ; inset : 0 ; display : grid ; place-items : center ; text-align : center ; }
. ring . c b { font : 700 24 px / 1 var ( -- mono ); display : block ; } . ring . c span { font-size : 11 px ; color : var ( -- dim ); }
. gpurows { flex : 1 ; display : flex ; flex-direction : column ; gap : 10 px ; min-width : 0 ; }
. gr . l { display : flex ; justify-content : space-between ; font-size : 12 px ; color : var ( -- dim ); margin-bottom : 4 px ; } . gr . l b { color : var ( -- text ); font : 500 12.5 px var ( -- mono ); }
. mini { height : 6 px ; background : #0d131d ; border-radius : 99 px ; overflow : hidden ; } . mini > div { height : 100 % ; border-radius : 99 px ; transition : width .6 s ; }
. gname { font-size : 12 px ; color : var ( -- dim2 ); margin-top : 12 px ; }
/* ---------- 指标卡 ---------- */
. kpis { display : grid ; grid-template-columns : repeat ( auto - fit , minmax ( 200 px , 1 fr )); gap : 16 px ; }
. kpi { position : relative ; overflow : hidden ; padding-bottom : 12 px ; }
. kpi . v { font : 650 26 px / 1.15 var ( -- mono ); letter-spacing : -.5 px ; } . kpi . v small { font-size : 13 px ; color : var ( -- dim ); margin-left : 4 px ; font-weight : 500 ; }
. kpi . s { color : var ( -- dim ); font-size : 12 px ; margin-top : 3 px ; } . kpi canvas { position : absolute ; right : 0 ; bottom : 0 ; width : 55 % ; height : 46 px ; opacity : .9 ; }
. delta { font : 500 12 px var ( -- mono ); margin-left : 6 px ; }
/* ---------- 图表 ---------- */
. charts { display : grid ; grid-template-columns : repeat ( auto - fit , minmax ( 520 px , 1 fr )); gap : 16 px ; }
. chart canvas . plot { width : 100 % ; height : 260 px ; display : block ; cursor : crosshair ; }
. legend { display : flex ; flex-wrap : wrap ; gap : 6 px 8 px ; margin-top : 10 px ; font-size : 12 px ; color : var ( -- dim ); }
. legend span { cursor : pointer ; user-select : none ; padding : 2 px 9 px ; border-radius : 99 px ; border : 1 px solid var ( -- line ); background : #0f1520 ; display : inline-flex ; align-items : center ; gap : 6 px ; }
. legend span b { font : 500 11.5 px var ( -- mono ); color : var ( -- text ); }
. legend span . off { opacity : .4 ; } . legend i { width : 8 px ; height : 8 px ; border-radius : 50 % ; display : inline-block ; }
/* ---------- 底部面板 ---------- */
. side { display : grid ; grid-template-columns : repeat ( auto - fit , minmax ( 380 px , 1 fr )); gap : 16 px ; }
table { width : 100 % ; border-collapse : collapse ; font-size : 13 px ; }
th , td { text-align : left ; padding : 7 px 6 px ; border-bottom : 1 px solid #1d2635 ; white-space : nowrap ; }
tr : last-child td { border-bottom : none ; }
th { color : var ( -- dim2 ); font-weight : 500 ; font-size : 12 px ; } td . n { font-family : var ( -- mono ); text-align : right ; } th . n { text-align : right ; }
. ok { color : var ( -- ok ); } . bad { color : var ( -- bad ); } . warn { color : var ( -- warn ); } . dim { color : var ( -- dim ); }
. chip { display : inline-block ; font-size : 11.5 px ; padding : 1 px 8 px ; border-radius : 99 px ; border : 1 px solid ; }
. chip . ok { border-color : #34d399 55 ; background : #34d399 14 ; } . chip . bad { border-color : #f87171 55 ; background : #f87171 14 ; } . chip . dim { border-color : var ( -- line2 ); }
. chip . warn { border-color : #fbbf24 55 ; background : #fbbf24 14 ; }
. empty { display : flex ; align-items : center ; gap : 10 px ; color : var ( -- dim2 ); font-size : 13 px ; padding : 18 px 4 px ; }
. empty svg { width : 18 px ; height : 18 px ; opacity : .6 ; }
. accbanner { display : flex ; align-items : center ; gap : 10 px ; padding : 10 px 12 px ; border-radius : 10 px ; margin-bottom : 8 px ; font-weight : 600 ; }
. accbanner . ok { background : #34d399 14 ; color : var ( -- ok ); border : 1 px solid #34d399 44 ; } . accbanner . bad { background : #f87171 14 ; color : var ( -- bad ); border : 1 px solid #f87171 44 ; }
. tip { position : fixed ; pointer-events : none ; background : #0a0e15 f2 ; border : 1 px solid var ( -- line2 ); border-radius : 10 px ; padding : 8 px 10 px ; font : 12 px / 1.6 var ( -- mono ); display : none ; z-index : 30 ; white-space : nowrap ; box-shadow : 0 10 px 30 px #0008 ; }
. tip . h { color : var ( -- dim ); margin-bottom : 2 px ; }
footer { color : var ( -- dim2 ); font-size : 12 px ; text-align : center ; padding-top : 6 px ; }
/* ---------- 控制:开始/停止、对话框、终端 ---------- */
button . primary { background : linear-gradient ( 135 deg , var ( -- accent ), var ( -- accent2 )); border : none ; color : #fff ; font-weight : 600 ; padding : 7 px 16 px ; box-shadow : 0 4 px 16 px #5aa9ff 40 ; }
button . primary : hover { filter : brightness ( 1.08 ); } button . primary : disabled { opacity : .45 ; cursor : not-allowed ; filter : none ; }
button . danger { color : var ( -- bad ); border-color : #f87171 66 ; } button . danger : hover { background : #f87171 14 ; }
. overall { display : flex ; align-items : flex-end ; justify-content : space-between ; gap : 16 px ; flex-wrap : wrap ; }
. overall . now { font-size : 13 px ; color : var ( -- dim ); } . overall . now b { display : block ; color : var ( -- text ); font-size : 18 px ; font-weight : 650 ; margin : 2 px 0 2 px ; }
. sublabel { font-size : 12.5 px ; color : var ( -- dim ); display : flex ; justify-content : space-between ; margin-top : 4 px ; }
. bar . thin { height : 8 px ; margin : 8 px 0 4 px ; } . bar . ind . fill { width : 30 % !important ; animation : ind 1.6 s ease-in-out infinite ; }
@ keyframes ind { 0 % { margin-left : 0 ; } 50 % { margin-left : 70 % ; } 100 % { margin-left : 0 ; } }
. ctlrow { display : flex ; gap : 10 px ; margin-top : 14 px ; align-items : center ; flex-wrap : wrap ; }
. ctlrow . hint { font-size : 12 px ; color : var ( -- dim2 ); }
. term { background : #070a10 ; border : 1 px solid var ( -- line ); border-radius : 10 px ; padding : 10 px 12 px ; height : 260 px ; overflow : auto ; font : 12 px / 1.55 var ( -- mono ); color : #c9d4e3 ; white-space : pre-wrap ; word-break : break-all ; }
. term . e { color : var ( -- bad ); } . term . c { color : var ( -- accent ); } . term . g { color : var ( -- ok ); }
. modal { position : fixed ; inset : 0 ; background : #0009 ; backdrop-filter : blur ( 3 px ); display : none ; align-items : center ; justify-content : center ; z-index : 40 ; }
. modal . show { display : flex ; }
. dlg { width : min ( 560 px , 92 vw ); background : #131a27 ; border : 1 px solid var ( -- line2 ); border-radius : 16 px ; padding : 20 px 22 px ; box-shadow : 0 30 px 80 px #000a ; }
. dlg h3 { margin : 0 0 4 px ; font-size : 16 px ; } . dlg . d { color : var ( -- dim ); font-size : 12.5 px ; margin-bottom : 14 px ; }
. form { display : grid ; grid-template-columns : 1 fr 1 fr ; gap : 12 px 14 px ; }
. form label { display : flex ; flex-direction : column ; gap : 5 px ; font-size : 12 px ; color : var ( -- dim ); }
. form input , . form select { background : #0f1520 ; color : var ( -- text ); border : 1 px solid var ( -- line2 ); border-radius : 8 px ; padding : 7 px 9 px ; font : 13 px var ( -- mono ); }
. form . full { grid-column : 1 / -1 ; }
2026-10-17 18:57:48 +00:00
. ckline { margin-top : 12 px ; font-size : 12 px ; color : var ( -- dim ); } . ckline . bad { color : var ( -- bad ); } . ckline . warn { color : var ( -- warn ); }
. demobar { background : #fbbf24 18 ; border : 1 px solid #fbbf24 55 ; color : var ( -- warn ); border-radius : 10 px ; padding : 8 px 12 px ; margin : 10 px 0 ; font-size : 13 px ; }
2026-10-17 17:15:16 +00:00
. dlg . acts { display : flex ; justify-content : flex-end ; gap : 10 px ; margin-top : 18 px ; }
. dlg . warnbox { margin-top : 12 px ; font-size : 12 px ; color : var ( -- warn ); background : #fbbf24 10 ; border : 1 px solid #fbbf24 33 ; border-radius : 8 px ; padding : 8 px 10 px ; display : none ; }
. toast { position : fixed ; bottom : 22 px ; left : 50 % ; transform : translateX ( -50 % ); background : #1b2433 ; border : 1 px solid var ( -- line2 ); padding : 9 px 16 px ; border-radius : 10 px ; font-size : 13 px ; z-index : 50 ; display : none ; box-shadow : 0 10 px 30 px #0008 ; }
@ media ( max-width : 900px ) { . hero { grid-template-columns : 1 fr ; } . charts { grid-template-columns : 1 fr ; } main { padding : 14 px ; } }
</ style >
</ head >
< body >
< header >
< div class = "brand" >
< div class = "logo" >< svg viewBox = "0 0 24 24" fill = "none" stroke = "#fff" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" >< path d = "M4 20h6" />< path d = "M7 20v-5l5-6" />< path d = "M12 9l5 2" />< circle cx = "12" cy = "9" r = "1.6" />< path d = "M17 11l2-3" /></ svg ></ div >
2026-10-18 06:23:31 +08:00
< div >< h1 > Transformer 多峰策略端到端训练与浏览器部署</ h1 >< div class = "sub" > 机械臂避障策略 · 训练监控</ div ></ div >
2026-10-17 17:15:16 +00:00
</ div >
< select id = "run" title = "选择运行(ml/runs/*) " ></ select >
< select id = "scenario" title = "示例场景" style = "display:none" >< option value = "train" > 示例:预训练进行中</ option >< option value = "gen" > 示例:生成数据中</ option >< option value = "late" > 示例:挖错第 3 轮</ option >< option value = "idle" > 示例:还没开始</ option ></ select >
< span id = "state" class = "pill" >< span class = "dot" ></ span >< span > 连接中</ span ></ span >
< span class = "sp" ></ span >
< label class = "ctl" > 平滑 < input id = "smooth" type = "range" min = "0" max = "0.99" step = "0.01" value = "0.85" >< span id = "smoothv" style = "font-family:var(--mono);width:30px" > 0.85</ span ></ label >
< button id = "logy" title = "损失图纵轴取对数" > 对数轴</ button >
< button id = "auto" class = "on" title = "每 5 秒刷新" >< span > ●</ span > 自动刷新</ button >
< button id = "btnStart" class = "primary" title = "一键运行流水线" > ▶ 开始训练</ button >
< button id = "btnStop" class = "danger" style = "display:none" title = "停止正在运行的流水线" > ■ 停止</ button >
</ header >
< main >
< section class = "card" >< h2 >< span > 流水线</ span >< span class = "r" id = "stepnote" ></ span ></ h2 >< div class = "steps" id = "steps" ></ div ></ section >
< section class = "hero" >
< div class = "card" id = "pipe" ></ div >
< div class = "card" >< h2 >< span > GPU</ span >< span class = "r" id = "gputime" ></ span ></ h2 >< div id = "gpu" ></ div ></ div >
</ section >
< section class = "card" id = "prog" ></ section >
< section class = "kpis" id = "kpis" ></ section >
< section class = "charts" id = "charts" ></ section >
< section class = "side" >
< div class = "card" >< h2 >< span > 数据阶段</ span >< span class = "r" > ml/data</ span ></ h2 >< div id = "data" ></ div ></ div >
< div class = "card" >< h2 >< span > M5 头门禁</ span >< span class = "r" > 纯策略穿柱率 ≤ 2%</ span ></ h2 >< div id = "m5" ></ div ></ div >
< div class = "card" >< h2 >< span > 挖错循环</ span >< span class = "r" > ml/runs/mine</ span ></ h2 >< div id = "mine" ></ div ></ div >
< div class = "card" >< h2 >< span > 部署形态评测</ span >< span class = "r" > 策略 + 安全层</ span ></ h2 >< div id = "bench" ></ div ></ div >
< div class = "card" >< h2 >< span > 验收汇总</ span >< span class = "r" > 方案 §9.2</ span ></ h2 >< div id = "accept" ></ div ></ div >
</ section >
< section class = "card" >< h2 >< span > 终端输出</ span >< span class = "r" id = "termnote" > ml/runs/console.log</ span ></ h2 >< div class = "term" id = "term" ></ div ></ section >
< footer id = "foot" ></ footer >
</ main >
< div class = "tip" id = "tip" ></ div >
< div class = "toast" id = "toast" ></ div >
< div class = "modal" id = "modal" >< div class = "dlg" >
< h3 > 开始训练</ h3 >
2026-10-17 18:57:48 +00:00
< div class = "d" > 后台执行 < code > bash ml/pipeline.sh all</ code > ,从对拍一路跑到验收。已完成的步骤自动跳过;训练每 10 分钟存档、生成按条续写,断了再点一次就从断点接着跑。网页和控制台窗口都可以关,训练不受影响。</ div >
2026-10-17 17:15:16 +00:00
< div class = "form" >
2026-10-17 18:57:48 +00:00
< label > 运行方式< select id = "fMode" >< option value = "resume" > 续跑:跳过已完成的步骤</ option >< option value = "fresh" > 从头重跑(旧产物移到 ml/archive/) </ option ></ select ></ label >
< label > 动作头 CFG< select id = "fCfg" >< option value = "ml/configs/base.json" > MeanFlow( base, M5 不过自动转 WTA) </ option >< option value = "ml/configs/wta.json" > WTA( wta) </ option >< option value = "ml/configs/small.json" > 小模型 d=128( R4 不过时用)</ option ></ select ></ label >
2026-10-17 17:15:16 +00:00
< label > 共识过滤 CONSENSUS< select id = "fCons" >< option value = "" > 按老师门禁自动</ option >< option value = "0" > 关</ option >< option value = "1" > 开(生成约慢 5 倍)</ option ></ select ></ label >
< label > 任务数 N_TASKS< input id = "fN" value = "50000" ></ label >
< label > 生成进程数 NPROC< input id = "fP" placeholder = "默认 = CPU 核数" ></ label >
< label > 目标样本 TARGET_SAMPLES< input id = "fT" value = "1700000" ></ label >
< label > 挖错轮数 ROUNDS< input id = "fR" value = "6" ></ label >
2026-10-17 18:57:48 +00:00
< label > 老师间隙下限 MIN_GAP_MM< input id = "fG" value = "3" ></ label >
2026-10-17 17:15:16 +00:00
</ div >
< div class = "warnbox" id = "fWarn" ></ div >
< div class = "acts" >< button id = "fCancel" > 取消</ button >< button id = "fGo" class = "primary" > ▶ 开始</ button ></ div >
</ div ></ div >
< script >
'use strict' ;
/* =====================================================================
数据层:python -m ml.monitor 提供 /api/runs、/api/log、/api/status。
直接双击打开本文件、地址带 ?demo=1,或 FORCE_DEMO=true 时用内置示例数据(每次刷新模拟训练推进)。
2026-10-18 新增(用户要求:训练中随时可视化查看进度)。
===================================================================== */
const FORCE_DEMO = false ;
const DEMO = FORCE_DEMO || new URLSearchParams ( location . search ). has ( 'demo' ) || location . protocol === 'file:' ;
const $ = id => document . getElementById ( id );
const isNum = v => typeof v === 'number' && isFinite ( v );
const fmt = ( v , d = 3 ) => ! isNum ( v ) ? '—' : ( Math . abs ( v ) >= 1000 ? Math . round ( v ). toLocaleString () : Math . abs ( v ) >= 100 ? v . toFixed ( 1 ) : Math . abs ( v ) >= 1 ? v . toFixed ( Math . min ( d , 3 )) : v === 0 ? '0' : v . toPrecision ( 3 ));
const pct = ( v , d = 1 ) => ! isNum ( v ) ? '—' : ( 100 * v ). toFixed ( d ) + '%' ;
const hms = s => { if ( ! isNum ( s )) return '—' ; s = Math . max ( 0 , s ); const h = Math . floor ( s / 3600 ), m = Math . floor ( s % 3600 / 60 ); return h ? ` ${ h } 小时 ${ m } 分` : m ? ` ${ m } 分 ${ Math . floor ( s % 60 ) } 秒` : ` ${ Math . floor ( s ) } 秒` ; };
const hmsShort = s => ! isNum ( s ) ? '—' : s >= 3600 ? ` ${ ( s / 3600 ). toFixed ( 1 ) } h` : s >= 60 ? ` ${ Math . round ( s / 60 ) } 分` : ` ${ Math . round ( s ) } 秒` ;
const ago = t => { const s = Date . now () / 1000 - t ; return s < 60 ? ` ${ Math . max ( 0 , Math . floor ( s )) } 秒前` : s < 3600 ? ` ${ Math . floor ( s / 60 ) } 分钟前` : ` ${ ( s / 3600 ). toFixed ( 1 ) } 小时前` ; };
const ICON_WAIT = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/></svg>' ;
const empty = t => `<div class="empty"> ${ ICON_WAIT } <span> ${ t } </span></div>` ;
const KIND = { train : [ '预训练' , '' ], apg : [ 'APG 微调' , 'apg' ], bench : [ '测速' , 'bench' ] };
const api = DEMO ? demoApi () : async ( path ) => { const r = await fetch ( path , { cache : 'no-store' }); if ( ! r . ok ) throw new Error ( r . status ); return r . json (); };
if ( DEMO ) $ ( 'scenario' ). style . display = '' ;
/* ===================== 图表:零依赖 canvas ===================== */
const css = v => getComputedStyle ( document . documentElement ). getPropertyValue ( v ). trim ();
const COLORS = [ '--c1' , '--c2' , '--c3' , '--c4' , '--c5' , '--c6' , '--c7' ]. map ( css );
const tip = $ ( 'tip' );
const hexA = ( hex , a ) => { const n = parseInt ( hex . slice ( 1 ), 16 ); return `rgba( ${ n >> 16 } , ${ n >> 8 & 255 } , ${ n & 255 } , ${ a } )` ; };
class Chart {
constructor ( parent , title , opts = {}) {
this . o = opts ; this . hidden = new Set ( opts . hidden || []);
const card = document . createElement ( 'div' ); card . className = 'card chart' ;
card . innerHTML = `<h2><span> ${ title } </span><span class="r"> ${ opts . note || '' } </span></h2><canvas class="plot"></canvas><div class="legend"></div>` ;
parent . appendChild ( card );
this . cv = card . querySelector ( 'canvas' ); this . lg = card . querySelector ( '.legend' ); this . series = [];
this . cv . addEventListener ( 'mousemove' , e => this . hover ( e ));
this . cv . addEventListener ( 'mouseleave' , () => { tip . style . display = 'none' ; this . hx = null ; this . draw (); });
new ResizeObserver (() => this . draw ()). observe ( this . cv );
}
set ( series ) {
this . series = series . map (( s , i ) => ({ ... s , color : s . color || COLORS [ i % COLORS . length ] }));
const last = s => { for ( let i = s . pts . length - 1 ; i >= 0 ; i -- ) if ( isNum ( s . pts [ i ][ 1 ])) return s . pts [ i ][ 1 ]; return null ; };
this . lg . innerHTML = this . series . filter ( s => ! s . noLegend ). map ( s => `<span data-k=" ${ s . name } " class=" ${ this . hidden . has ( s . name ) ? 'off' : '' } "><i style="background: ${ s . color } "></i> ${ s . label || s . name } <b> ${ this . o . percent ? pct ( last ( s )) : fmt ( last ( s )) } </b></span>` ). join ( '' );
this . lg . querySelectorAll ( 'span' ). forEach ( el => el . onclick = () => { const k = el . dataset . k ; this . hidden . has ( k ) ? this . hidden . delete ( k ) : this . hidden . add ( k ); el . classList . toggle ( 'off' ); this . draw (); });
this . draw ();
}
vis () { return this . series . filter ( s => ! this . hidden . has ( s . name ) && s . pts . length ); }
draw () {
const cv = this . cv , dpr = devicePixelRatio || 1 , W = cv . clientWidth , H = cv . clientHeight ;
if ( ! W ) return ;
cv . width = W * dpr ; cv . height = H * dpr ;
const g = cv . getContext ( '2d' ); g . scale ( dpr , dpr ); g . clearRect ( 0 , 0 , W , H );
const S = this . vis (), L = 54 , R = 14 , T = 10 , B = 24 ;
g . font = '11px ui-monospace, Menlo, Consolas, monospace' ;
if ( ! S . length ) { g . fillStyle = css ( '--dim2' ); g . textAlign = 'center' ; g . fillText ( this . o . emptyText || '暂无数据' , W / 2 , H / 2 ); this . map = null ; return ; }
const logy = this . o . logy && this . o . logy ();
const ty = v => logy ? Math . log10 ( Math . max ( v , 1e-12 )) : v ;
let x0 = Infinity , x1 = - Infinity , y0 = Infinity , y1 = - Infinity ;
for ( const s of S ) for ( const [ x , y ] of [... s . pts , ...( s . raw || [])]) { if ( ! isNum ( y )) continue ; x0 = Math . min ( x0 , x ); x1 = Math . max ( x1 , x ); if ( s . noScale ) continue ; const v = ty ( y ); y0 = Math . min ( y0 , v ); y1 = Math . max ( y1 , v ); }
if ( this . o . xmax && this . o . xmax ()) x1 = Math . max ( x1 , this . o . xmax ());
if ( this . o . xmin0 ) x0 = Math . min ( 0 , x0 );
if ( this . o . range ) [ y0 , y1 ] = this . o . range ;
if ( this . o . zero && ! logy ) y0 = Math . min ( 0 , y0 );
if ( ! isFinite ( y0 )) { y0 = 0 ; y1 = 1 ; }
if ( y1 - y0 < 1e-12 ) { y1 += 0.5 ; y0 -= 0.5 ; }
if ( ! this . o . range ) { const pad = ( y1 - y0 ) * 0.08 ; y1 += pad ; if ( ! ( this . o . zero && ! logy && y0 === 0 )) y0 -= pad ; }
if ( x1 === x0 ) x1 = x0 + 1 ;
const X = x => L + ( x - x0 ) / ( x1 - x0 ) * ( W - L - R ), Y = v => T + ( 1 - ( v - y0 ) / ( y1 - y0 )) * ( H - T - B );
this . map = { X , Y , x0 , x1 , L , R , W , ty };
// 网格与刻度
g . strokeStyle = '#1e2838' ; g . lineWidth = 1 ; g . fillStyle = css ( '--dim2' ); g . textAlign = 'right' ;
for ( let i = 0 ; i <= 4 ; i ++ ) {
const v = y0 + ( y1 - y0 ) * i / 4 , y = Math . round ( Y ( v )) + . 5 ;
g . beginPath (); g . moveTo ( L , y ); g . lineTo ( W - R , y ); g . stroke ();
g . fillText ( logy ? fmt ( 10 ** v ) : this . o . percent ? ( 100 * v ). toFixed ( 0 ) + '%' : fmt ( v ), L - 8 , y + 4 );
}
g . textAlign = 'center' ;
for ( let i = 0 ; i <= 5 ; i ++ ) { const x = x0 + ( x1 - x0 ) * i / 5 ; const lab = x >= 1000 ? ( x / 1000 ). toFixed ( x1 - x0 < 5000 ? 1 : 0 ) + 'k' : x . toFixed ( 0 ); g . fillText ( lab , Math . max ( L + 10 , Math . min ( X ( x ), W - R - 12 )), H - 6 ); }
// 未到达区域(总步数之后)淡色
const lastX = Math . max (... S . map ( s => s . pts . length ? s . pts [ s . pts . length - 1 ][ 0 ] : - Infinity ));
if ( this . o . xmax && lastX < x1 ) { g . fillStyle = 'rgba(255,255,255,0.018)' ; g . fillRect ( X ( lastX ), T , X ( x1 ) - X ( lastX ), H - T - B ); }
// 参考线
for ( const ref of this . o . refs || []) {
const y = Y ( ty ( ref . y )); g . strokeStyle = hexA ( ref . color , . 7 ); g . setLineDash ([ 4 , 4 ]); g . beginPath (); g . moveTo ( L , y ); g . lineTo ( W - R , y ); g . stroke (); g . setLineDash ([]);
g . fillStyle = ref . color ; g . textAlign = 'left' ; g . fillText ( ref . label , L + 6 , y - 5 );
}
const path = ( pts ) => { g . beginPath (); let f = true ; for ( const [ x , y ] of pts ) { if ( ! isNum ( y )) { f = true ; continue ; } const px = X ( x ), py = Y ( ty ( y )); f ? g . moveTo ( px , py ) : g . lineTo ( px , py ); f = false ; } };
for ( const s of S ) {
if ( s . raw ) { path ( s . raw ); g . strokeStyle = hexA ( s . color , . 22 ); g . lineWidth = 1 ; g . stroke (); }
if ( s . area ) {
path ( s . pts ); const pts = s . pts . filter ( p => isNum ( p [ 1 ]));
if ( pts . length > 1 ) {
g . lineTo ( X ( pts [ pts . length - 1 ][ 0 ]), H - B ); g . lineTo ( X ( pts [ 0 ][ 0 ]), H - B ); g . closePath ();
const gr = g . createLinearGradient ( 0 , T , 0 , H - B ); gr . addColorStop ( 0 , hexA ( s . color , . 28 )); gr . addColorStop ( 1 , hexA ( s . color , 0 )); g . fillStyle = gr ; g . fill ();
}
}
path ( s . pts ); g . strokeStyle = s . color ; g . lineWidth = s . width || 2 ; g . lineJoin = 'round' ; g . lineCap = 'round' ; g . setLineDash ( s . dash || []); g . stroke (); g . setLineDash ([]);
if ( s . dots ) for ( const [ x , y ] of s . pts ) if ( isNum ( y )) { g . beginPath (); g . arc ( X ( x ), Y ( ty ( y )), 3 , 0 , 7 ); g . fillStyle = '#0b0f17' ; g . fill (); g . lineWidth = 1.8 ; g . strokeStyle = s . color ; g . stroke (); }
if ( s . best ) { const [ bx , by ] = s . best ; g . beginPath (); g . arc ( X ( bx ), Y ( ty ( by )), 6 , 0 , 7 ); g . strokeStyle = s . color ; g . lineWidth = 1.5 ; g . stroke (); g . fillStyle = s . color ; g . textAlign = 'left' ; g . fillText ( '最好' , X ( bx ) + 9 , Y ( ty ( by )) - 6 ); }
}
if ( this . hx != null ) { g . strokeStyle = '#8c99ad88' ; g . setLineDash ([ 3 , 3 ]); g . beginPath (); g . moveTo ( X ( this . hx ), T ); g . lineTo ( X ( this . hx ), H - B ); g . stroke (); g . setLineDash ([]);
for ( const s of S ) { const p = nearest ( s . pts , this . hx ); if ( p && isNum ( p [ 1 ])) { g . beginPath (); g . arc ( X ( p [ 0 ]), Y ( ty ( p [ 1 ])), 4 , 0 , 7 ); g . fillStyle = s . color ; g . fill (); } } }
}
hover ( e ) {
if ( ! this . map ) return ;
const r = this . cv . getBoundingClientRect (), { x0 , x1 , L , R , W } = this . map ;
const x = x0 + ( e . clientX - r . left - L ) / ( W - L - R ) * ( x1 - x0 );
const rows = []; let hx = null ;
for ( const s of this . vis ()) { if ( s . noLegend ) continue ; const p = nearest ( s . pts , x ); if ( p ) { hx = hx ?? p [ 0 ]; rows . push ( `<span style="color: ${ s . color } ">●</span> ${ s . label || s . name } ${ this . o . percent ? pct ( p [ 1 ], 2 ) : fmt ( p [ 1 ], 4 ) } ` ); } }
this . hx = hx ; this . draw ();
if ( ! rows . length ) return ;
tip . innerHTML = `<div class="h">第 ${ hx . toLocaleString () } 步</div>` + rows . join ( '<br>' ); tip . style . display = 'block' ;
tip . style . left = Math . min ( e . clientX + 16 , innerWidth - tip . offsetWidth - 10 ) + 'px' ; tip . style . top = ( e . clientY + 16 ) + 'px' ;
}
}
function nearest ( pts , x ) { let lo = 0 , hi = pts . length - 1 ; if ( hi < 0 ) return null ; while ( hi - lo > 1 ) { const m = ( lo + hi ) >> 1 ; pts [ m ][ 0 ] < x ? lo = m : hi = m ; } return Math . abs ( pts [ lo ][ 0 ] - x ) <= Math . abs ( pts [ hi ][ 0 ] - x ) ? pts [ lo ] : pts [ hi ]; }
function spark ( cv , pts , color ) {
const dpr = devicePixelRatio || 1 , W = cv . clientWidth , H = cv . clientHeight ; if ( ! W || pts . length < 2 ) return ;
cv . width = W * dpr ; cv . height = H * dpr ; const g = cv . getContext ( '2d' ); g . scale ( dpr , dpr );
const ys = pts . map ( p => p [ 1 ]). filter ( isNum ); const lo = Math . min (... ys ), hi = Math . max (... ys ) || 1 , x0 = pts [ 0 ][ 0 ], x1 = pts [ pts . length - 1 ][ 0 ] || 1 ;
const X = x => ( x - x0 ) / ( x1 - x0 || 1 ) * W , Y = y => H - 4 - ( y - lo ) / (( hi - lo ) || 1 ) * ( H - 10 );
g . beginPath (); pts . forEach (([ x , y ], i ) => i ? g . lineTo ( X ( x ), Y ( y )) : g . moveTo ( X ( x ), Y ( y )));
g . strokeStyle = color ; g . lineWidth = 1.6 ; g . stroke (); g . lineTo ( W , H ); g . lineTo ( 0 , H ); g . closePath ();
const gr = g . createLinearGradient ( 0 , 0 , 0 , H ); gr . addColorStop ( 0 , hexA ( color , . 25 )); gr . addColorStop ( 1 , hexA ( color , 0 )); g . fillStyle = gr ; g . fill ();
}
const thin = ( pts , n = 400 ) => { if ( pts . length <= n ) return pts ; const k = pts . length / n , o = []; for ( let i = 0 ; i < n ; i ++ ) o . push ( pts [ Math . floor ( i * k )]); o . push ( pts [ pts . length - 1 ]); return o ; };
/* ===================== 状态 ===================== */
let cur = null , recs = [], evals = [], offset = 0 , timer = null , charts = null , lastMtime = 0 , lastStatus = null , runsCache = [];
let logyOn = false , job = null , userPicked = false , conOff = 0 ;
2026-10-17 18:57:48 +00:00
/* 阶段表:名称、粗估耗时(小时,作全流程百分比权重)、超时上限。
2026-10-18 第五轮(审查 N-14):真实模式下由 /api/status 的 stage_meta 下发(来源 ml/pstate.py STAGES,与超时同源);
下面这份只给示例数据用,数值与 pstate.py 一致。 */
let STAGE_META = [
[ 'setup' , '安装依赖' , 0.2 ], [ 'kin' , '对拍' , 0.2 ], [ 'r4' , '推理延迟' , 0.2 ], [ 'gate1' , '一致性门禁' , 0.5 ], [ 'gen' , '生成老师轨迹' , 5.0 ],
[ 'prep' , '打包样本' , 0.5 ], [ 'train' , '预训练' , 3.0 ], [ 'm5' , '头门禁' , 0.5 ], [ 'apg' , 'APG 微调' , 4.0 ], [ 'mine' , '挖错循环' , 9.0 ],
[ 'export' , '导出' , 0.1 ], [ 'bench' , '部署评测' , 1.0 ], [ 'accept' , '验收汇总' , 0.5 ]];
let SM = Object . fromEntries ( STAGE_META . map (([ k , n , w ]) => [ k , { n , w , to : Math . max ( 1 , 3 * w ) }]));
function applyStageMeta ( m ) {
if ( ! m || ! m . length ) return ;
STAGE_META = m . map ( x => [ x . id , x . name , x . est_h ]); SM = Object . fromEntries ( m . map ( x => [ x . id , { n : x . name , w : x . est_h , to : x . timeout_h }]));
}
/* 开始 / 停止请求带自定义头:跨站网页发不出这种请求(会触发 CORS 预检,控制台不应答),见 monitor.py 审查 N-8 */
const post = ( path , body ) => fetch ( path , { method : 'POST' , headers : { 'X-Console' : '1' , 'Content-Type' : 'application/json' }, body : body || '{}' }). then ( x => x . json ()). catch ( e => ({ error : String ( e ) }));
2026-10-17 17:15:16 +00:00
const smoothv = () => + $ ( 'smooth' ). value ;
const ema = ( pts , a ) => { if ( ! a ) return pts ; let m = null ; return pts . map (([ x , y ]) => { if ( ! isNum ( y )) return [ x , y ]; m = m == null ? y : a * m + ( 1 - a ) * y ; return [ x , m ]; }); };
const xOf = r => r . step ?? r . it ?? 0 ;
function buildCharts ( kind ) {
const c = $ ( 'charts' ); c . innerHTML = '' ;
const total = () => { const r = recs [ recs . length - 1 ]; return r && r . total ; };
if ( kind === 'apg' ) {
charts = {
a : new Chart ( c , '到达误差(mm,展开末)' , { logy : () => logyOn , xmax : total , xmin0 : true , note : '越低越好 · 浅色为原始值' }),
b : new Chart ( c , '损失分量' , { logy : () => logyOn , xmax : total , xmin0 : true }),
e : new Chart ( c , '闭环评估(val,部署节奏)' , { percent : true , range : [ 0 , 1 ], xmax : total , xmin0 : true , note : '每 500 步' , emptyText : '第一次评估在第 500 步' }),
t : new Chart ( c , '展开长度与梯度' , { xmax : total , xmin0 : true , zero : true , note : '展开 60– 150 次推理随批变化' }),
};
} else {
charts = {
a : new Chart ( c , '总损失' , { logy : () => logyOn , xmax : total , xmin0 : true , note : '浅色为原始值' }),
b : new Chart ( c , '损失分量' , { logy : () => logyOn , xmax : total , xmin0 : true , hidden : [ 'smooth' ] }),
e : new Chart ( c , '闭环评估(val,部署节奏,关安全层)' , { percent : true , range : [ 0 , 1 ], xmax : total , xmin0 : true , note : kind === 'bench' ? '测速运行不评估' : '每 2000 步' ,
emptyText : kind === 'bench' ? '测速运行不做闭环评估' : '第一次评估在第 2000 步' }),
t : new Chart ( c , '吞吐(样本/s) ' , { xmax : total , xmin0 : true , zero : true , refs : [{ y : 5000 , label : 'R10 通过线 5000' , color : css ( '--bad' ) }] }),
};
}
}
function render () {
if ( ! cur || ! charts ) return ;
const a = smoothv (), kind = cur . kind ;
const raw = k => thin ( recs . filter ( r => isNum ( r [ k ])). map ( r => [ xOf ( r ), r [ k ]]));
const S = ( k , label , extra = {}) => ({ name : k , label : label || k , pts : thin ( ema ( recs . filter ( r => isNum ( r [ k ])). map ( r => [ xOf ( r ), r [ k ]]), a )), ... extra });
if ( kind === 'apg' ) {
charts . a . set ([ S ( 'err_mm_mean' , '平均到达误差' , { raw : raw ( 'err_mm_mean' ), area : true })]);
charts . b . set ([ S ( 'tgt' , '到达项' ), S ( 'col' , '碰撞铰链' ), S ( 'bc' , 'BC 锚定' ), S ( 'smooth' , '平滑 /10' , { pts : thin ( ema ( recs . filter ( r => isNum ( r . smooth )). map ( r => [ xOf ( r ), r . smooth / 10 ]), a )) })]);
charts . t . set ([{ name : 'plans' , label : '展开推理次数' , pts : thin ( recs . filter ( r => r . plans ). map ( r => [ xOf ( r ), r . plans ])), width : 1.4 }, S ( 'gnorm' , '梯度范数 × 50' , { pts : thin ( ema ( recs . filter ( r => isNum ( r . gnorm )). map ( r => [ xOf ( r ), r . gnorm * 50 ]), a )) })]);
} else {
charts . a . set ([ S ( 'loss' , '总损失' , { raw : raw ( 'loss' ), area : true })]);
charts . b . set ([ S ( 'mf' , cur . name . includes ( 'wta' ) ? 'WTA' : 'MeanFlow' ), S ( 'fk_cm' , 'FK( cm) ' ), S ( 'value' , '价值头' ), S ( 'sub' , '子目标头' ), S ( 'smooth' , '平滑' )]);
charts . t . set ([ S ( 'samples_per_s' , '样本/s' , { area : true })]);
}
const E = ( k , label , best ) => { const pts = evals . filter ( e => isNum ( e [ k ])). map ( e => [ e . x , e [ k ]]); let b = null ; if ( best && pts . length ) b = pts . reduce (( m , p ) => p [ 1 ] > m [ 1 ] ? p : m ); return { name : k , label , dots : true , pts , best : b }; };
charts . e . set ([ E ( 'success' , '到达率' , true ), E ( 'collision_rate' , '碰撞率' ), E ( 'time_ok_1p3' , '≤ 老师时长×1.3' ), E ( 'start_fail' , '起步失败' )]);
renderProgress (); renderKpis ();
}
function renderProgress () {
const r = recs [ recs . length - 1 ] || {}, kind = cur . kind ;
const step = xOf ( r ), total = r . total , done = total ? Math . min ( 1 , step / total ) : null ;
const t0 = recs . find ( v => v . t ) ? . t , elapsed = r . t && t0 ? r . t - t0 : ( isNum ( r . min ) ? r . min * 60 : null );
let eta = isNum ( r . eta_h ) ? r . eta_h * 3600 : null ;
if ( eta == null && elapsed && done ) eta = elapsed / done * ( 1 - done );
const running = ! DEMO ? ( Date . now () / 1000 - lastMtime < 120 ) : cur . live ;
const finished = done != null && done >= 0.999 ;
const bestE = evals . length ? evals . reduce (( m , e ) => ( e . success ?? - 1 ) > ( m . success ?? - 1 ) ? e : m ) : null ;
const marks = total ? evals . map ( e => `<div class="mk ${ bestE && e === bestE ? 'best' : '' } " style="left: ${ 100 * e . x / total } %" title="评估 @ ${ e . x } :到达 ${ pct ( e . success ) } "></div>` ). join ( '' ) : '' ;
const [ kl , kc ] = KIND [ kind ];
const fin = eta != null && running && ! finished ? new Date ( Date . now () + eta * 1000 ). toLocaleString ( 'zh-CN' , { hour12 : false , month : 'numeric' , day : 'numeric' , hour : '2-digit' , minute : '2-digit' }) : '—' ;
const fact = ( k , v , cls = '' ) => `<div class="fact"><div class="k"> ${ k } </div><div class="v ${ cls } "> ${ v } </div></div>` ;
$ ( 'prog' ). innerHTML = `<h2><span>当前运行 · 训练曲线</span><span class="r"> ${ finished ? '已完成' : running ? '运行中' : '未在运行' } </span></h2>
<div class="prog-top"><div class="ttl"><b> ${ cur . name } </b><span class="tag ${ kc } "> ${ kl } </span> ${ cur . diverged ? ' <span class="chip bad">已发散 · diverged.pt</span>' : '' } </div>
<div class="bigpct"> ${ done != null ? ( 100 * done ). toFixed ( 1 ) : '—' } <small>%</small></div></div>
<div class="bar ${ running && ! finished ? '' : 'idle' } "><div class="fill" style="width: ${ done != null ? 100 * done : 0 } %"></div> ${ marks } </div>
<div class="facts">
${ fact ( '步数' , ` ${ step . toLocaleString () }${ total ? ' / ' + total . toLocaleString () : '' } ` ) }
${ fact ( '已用时间' , hms ( elapsed )) }
${ fact ( '剩余时间' , finished ? '—' : hms ( eta )) }
${ fact ( '预计完成' , fin ) }
${ kind === 'apg' ? fact ( '本批展开' , r . plans ? ` ${ r . plans } 次推理` : '—' ) : fact ( '学习率' , isNum ( r . lr ) ? r . lr . toExponential ( 2 ) : '—' ) }
${ fact ( '日志更新' , lastMtime ? ago ( lastMtime ) : '—' , running ? '' : 'warn' ) }
</div>` ;
}
function renderKpis () {
const r = recs [ recs . length - 1 ] || {}, e = evals [ evals . length - 1 ] || {}, pe = evals [ evals . length - 2 ] || {}, kind = cur . kind ;
const best = evals . length ? Math . max (... evals . map ( v => v . success ?? - 1 )) : null ;
const dlt = ( k , good = 1 ) => { if ( ! isNum ( e [ k ]) || ! isNum ( pe [ k ])) return '' ; const d = ( e [ k ] - pe [ k ]) * 100 ; if ( Math . abs ( d ) < 0.05 ) return '' ; const up = d > 0 ; return `<span class="delta ${ ( up ? 1 : - 1 ) * good > 0 ? 'ok' : 'bad' } "> ${ up ? '▲' : '▼' } ${ Math . abs ( d ). toFixed ( 1 ) } pp</span>` ; };
const last = k => recs . filter ( v => isNum ( v [ k ])). slice ( - 300 ). map ( v => [ xOf ( v ), v [ k ]]);
const evs = k => evals . filter ( v => isNum ( v [ k ])). map ( v => [ v . x , v [ k ]]);
const tiles = [];
const tile = ( label , v , s , cls , sp , color ) => tiles . push ({ html : `<div class="card kpi"><h2><span> ${ label } </span></h2><div class="v ${ cls || '' } "> ${ v } </div><div class="s"> ${ s || ' ' } </div><canvas></canvas></div>` , sp , color });
if ( kind === 'apg' ) {
tile ( '到达误差' , ` ${ fmt ( r . err_mm_mean ) } <small>mm</small>` , '展开末平均' , '' , last ( 'err_mm_mean' ), COLORS [ 0 ]);
tile ( '展开内到达比例' , pct ( r . reach_frac ), '误差 < 1 mm 且姿态 < 0.5°' , '' , last ( 'reach_frac' ), COLORS [ 2 ]);
tile ( '闭环到达率' , pct ( e . success ) + dlt ( 'success' ), `最好 ${ pct ( best ) } ` , '' , evs ( 'success' ), COLORS [ 2 ]);
tile ( '碰撞率' , pct ( e . collision_rate , 2 ) + dlt ( 'collision_rate' , - 1 ), '关安全层' , e . collision_rate > 0.03 ? 'warn' : '' , evs ( 'collision_rate' ), COLORS [ 3 ]);
2026-10-18 06:34:21 +08:00
tile ( '显存峰值' , isNum ( r . vram ) ? ` ${ r . vram . toFixed ( 2 ) } <small>GiB</small>` : '—' , '预算 19– 20 GB(整卡 24,桌面占 4– 5) ' , r . vram > 17 ? 'bad' : r . vram > 14 ? 'warn' : '' , last ( 'vram' ), COLORS [ 1 ]);
2026-10-17 17:15:16 +00:00
} else {
const sps = r . samples_per_s ;
tile ( '总损失' , fmt ( r . loss , 4 ), `MF ${ fmt ( r . mf ) } · FK ${ fmt ( r . fk_cm ) } cm` , '' , ema ( last ( 'loss' ), 0.9 ), COLORS [ 0 ]);
tile ( '吞吐' , sps ? ` ${ ( sps / 1000 ). toFixed ( 2 ) } <small>k 样本/s</small>` : '—' , sps ? ( sps >= 5000 ? 'R10 达标(≥ 5k/s) ' : 'R10 未达标(< 5k/s) ' ) : 'R10 通过线 5k/s' , sps ? ( sps >= 5000 ? 'ok' : 'bad' ) : '' , last ( 'samples_per_s' ), COLORS [ 5 ]);
tile ( '闭环到达率' , pct ( e . success ) + dlt ( 'success' ), evals . length ? `最好 ${ pct ( best ) } · @ ${ e . x . toLocaleString () } 步` : '尚未评估' , '' , evs ( 'success' ), COLORS [ 2 ]);
tile ( '碰撞率' , pct ( e . collision_rate , 2 ) + dlt ( 'collision_rate' , - 1 ), '关安全层,部署另有护栏' , e . collision_rate > 0.03 ? 'warn' : '' , evs ( 'collision_rate' ), COLORS [ 3 ]);
tile ( '梯度范数' , fmt ( r . gnorm ), '裁剪阈值 1.0' , r . gnorm > 5 ? 'warn' : '' , ema ( last ( 'gnorm' ), 0.9 ), COLORS [ 4 ]);
2026-10-18 06:34:21 +08:00
tile ( '显存峰值' , isNum ( r . vram ) ? ` ${ r . vram . toFixed ( 2 ) } <small>GiB</small>` : '—' , '预算 19– 20 GB(整卡 24,桌面占 4– 5) ' , r . vram > 17 ? 'bad' : r . vram > 14 ? 'warn' : '' , last ( 'vram' ), COLORS [ 1 ]);
2026-10-17 17:15:16 +00:00
}
$ ( 'kpis' ). innerHTML = tiles . map ( t => t . html ). join ( '' );
[... $ ( 'kpis' ). querySelectorAll ( 'canvas' )]. forEach (( cv , i ) => spark ( cv , tiles [ i ]. sp , tiles [ i ]. color ));
}
/* ===================== 轮询 ===================== */
async function loadRuns () {
const runs = await api ( '/api/runs' ); runsCache = runs ;
const sel = $ ( 'run' ), prev = sel . value ;
sel . innerHTML = runs . map ( r => `<option value=" ${ r . name } "> ${ r . name } · ${ KIND [ r . kind ][ 0 ] }${ r . diverged ? ' · 发散' : '' } </option>` ). join ( '' ) || '<option value="">ml/runs 下还没有日志</option>' ;
const follow = lastStatus && lastStatus . pipeline && lastStatus . pipeline . progress && lastStatus . pipeline . progress . run ;
if ( ! userPicked ) sel . value = runs . some ( r => r . name === follow ) ? follow : ( runs [ 0 ] ? runs [ 0 ]. name : '' ); // 自动跟随正在跑的那条
else if ( runs . some ( r => r . name === prev )) sel . value = prev ;
const r = runs . find ( v => v . name === sel . value ) || runs [ 0 ];
if ( r && ( ! cur || cur . name !== r . name )) { cur = { ... r }; recs = []; evals = []; offset = 0 ; buildCharts ( r . kind ); }
else if ( r ) Object . assign ( cur , r );
if ( ! r ) { cur = null ; $ ( 'charts' ). innerHTML = '' ; $ ( 'kpis' ). innerHTML = '' ; $ ( 'prog' ). innerHTML = '<h2><span>当前运行 · 训练曲线</span></h2>' + empty ( '还没有训练日志:预训练开始后这里出现损失、吞吐和评估曲线。' ); }
}
async function poll () {
try {
2026-10-17 18:57:48 +00:00
lastStatus = await api ( '/api/status' ); applyStageMeta ( lastStatus . stage_meta );
2026-10-17 17:15:16 +00:00
job = await api ( '/api/job' ). catch (() => null );
await loadRuns ();
if ( cur ) {
const d = await api ( `/api/log?run= ${ encodeURIComponent ( cur . name ) } &offset= ${ offset } ` );
if ( d . reset && offset ) { recs = []; evals = []; }
for ( const r of d . records || []) {
if ( r . eval ) { const e = r . eval ; evals . push ({ ... e , x : e . step ?? e . it ?? r . step ?? 0 }); } else recs . push ( r );
}
offset = d . offset ?? offset ; lastMtime = d . mtime || lastMtime ; cur . live = d . live ;
render ();
}
const P = lastStatus . pipeline , busy = ( job && job . running ) || ( P && P . current );
2026-10-18 06:34:21 +08:00
if ( DEMO ) { setPill ( 'demo' , '示例数据' ); if ( ! $ ( 'demobar' )) $ ( 'pipe' ). closest ( '.hero' ). insertAdjacentHTML ( 'beforebegin' , `<div class="demobar" id="demobar">这是示例数据,不是你的训练。 ${ location . protocol === 'file:' ? '直接双击 monitor.html 只能看示例;' : '' } 看真实训练请双击 ml/console.bat(或 python -m ml.monitor --open)。</div>` ); }
2026-10-17 17:15:16 +00:00
else if ( busy ) setPill ( 'live' , `运行中 · ${ P && P . current ? SM [ P . current ] ? . n || P . current : '启动中' } ` );
else if ( cur && Date . now () / 1000 - lastMtime < 120 ) setPill ( 'live' , `运行中 · ${ ago ( lastMtime ) } 更新` );
else setPill ( '' , '空闲' );
renderStatus ( lastStatus ); renderPipe ( lastStatus ); renderJob ();
await pollConsole ();
} catch ( err ) {
setPill ( 'dead' , '连不上控制台(双击 ml/console.bat 或 python -m ml.monitor) ' ); console . error ( err );
}
}
function setPill ( cls , text ) { const st = $ ( 'state' ); st . className = 'pill ' + cls ; st . lastElementChild . textContent = text ; }
/* ===================== 流水线 / GPU / 各阶段面板 ===================== */
function stages ( s ) {
const P = s . pipeline ;
if ( P ) { // 有流水线记录:以它为准
const plan = P . plan && P . plan . length ? P . plan : STAGE_META . map ( x => x [ 0 ]). filter ( k => k !== 'setup' );
const keys = STAGE_META . map ( x => x [ 0 ]). filter ( k => plan . includes ( k ) || P . stages [ k ]);
return keys . map ( k => {
const r = P . stages [ k ], nm = SM [ k ]. n ;
if ( ! r ) return [ nm , 'pending' , '—' ];
if ( P . current === k ) return [ nm , 'active' , P . progress && P . progress . frac != null ? pct ( P . progress . frac , 0 ) : '进行中' ];
if ( r . rc === 0 ) return [ nm , 'done' , '用时 ' + hmsShort ( r . end - r . start )];
if ( r . rc === - 15 || r . rc === 143 ) return [ nm , 'fail' , '已停止' ];
if ( r . rc != null ) return [ nm , 'fail' , `失败(返回码 ${ r . rc } ) `];
return [ nm , 'fail' , '中断' ];
});
}
const D = s . data || {}, has = n => runsCache . find ( r => r . name === n || r . name . startsWith ( n ));
const live = n => { const r = has ( n ); return r && Date . now () / 1000 - r . mtime < 120 ; };
const anyTrain = runsCache . find ( r => r . kind === 'train' );
const m5ok = s . m5 && s . m5 . some ( m => m . pass ), m5fail = s . m5 && s . m5 . length && ! m5ok ;
const mineN = ( s . mine || []). length , mineDone = ( s . mine || []). filter ( m => m . gate && m . gate . accept != null ). length ;
const L = [
[ '对拍' , D . kin_pass != null ? ( D . kin_pass ? 'done' : 'fail' ) : ( D . consistency != null ? 'done' : 'pending' ), '—' ],
[ '老师门禁' , D . consistency != null ? 'done' : 'pending' , D . consistency != null ? `一致率 ${ pct ( D . consistency , 0 ) } ` : '—' ],
[ '生成数据' , D . stats ? 'done' : D . gen_tasks_written ? 'active' : 'pending' , D . stats ? ` ${ ( D . stats . tasks / 1000 ). toFixed ( 1 ) } k 条` : D . gen_tasks_written ? ` ${ D . gen_tasks_written . toLocaleString () } 条…` : '—' ],
[ '打包样本' , D . index ? 'done' : 'pending' , D . index ? ` ${ ( D . index . train_samples / 1e6 ). toFixed ( 2 ) } M 样本` : '—' ],
[ '预训练' , s . m5 && s . m5 . length ? 'done' : anyTrain ? ( live ( anyTrain . name ) ? 'active' : 'done' ) : 'pending' , anyTrain ? anyTrain . name : '—' ],
[ 'M5 门禁' , m5ok ? 'done' : m5fail ? 'fail' : 'pending' , m5ok ? '通过' : m5fail ? '未通过' : '—' ],
[ 'APG 微调' , has ( 'apg' ) && ! has ( 'apg_r' ) ? ( live ( 'apg' ) ? 'active' : 'done' ) : has ( 'apg_r' ) || mineN ? 'done' : 'pending' , has ( 'apg' ) ? 'apg' : '—' ],
[ '挖错循环' , mineN ? ( mineDone === mineN && ! live ( 'apg_r' ) ? 'done' : 'active' ) : 'pending' , mineN ? `第 ${ mineN } 轮` : '—' ],
[ '导出/评测' , s . bench ? 'done' : 'pending' , s . bench ? '已评测' : '—' ],
[ '验收汇总' , s . acceptance ? ( s . acceptance . all_pass ? 'done' : 'fail' ) : 'pending' , s . acceptance ? ( s . acceptance . all_pass ? '全部通过' : ` ${ s . acceptance . failed . length } 项未过` ) : '—' ],
];
return L ;
}
function renderStatus ( s ) {
const L = stages ( s );
$ ( 'steps' ). innerHTML = L . map (([ nm , st , ds ], i ) => `<div class="step ${ st } "><div class="ic"> ${ st === 'done' ? '✓' : st === 'fail' ? '!' : i + 1 } </div><div class="nm"> ${ nm } </div><div class="ds"> ${ ds } </div></div>` ). join ( '' );
const nDone = L . filter ( x => x [ 1 ] === 'done' ). length ; $ ( 'stepnote' ). textContent = ` ${ nDone } / ${ L . length } 完成` ;
// GPU
const G = s . gpu || [];
2026-10-18 06:34:21 +08:00
const BUD = ( s . vram_budget_gb || 24 ) * 1024 ; // 显存预算(MB):桌面常驻占 4–5 GB,不能按整卡 24 GB 看
2026-10-17 17:15:16 +00:00
$ ( 'gputime' ). textContent = G . length ? '实时' : '' ;
$ ( 'gpu' ). innerHTML = G . length ? G . map ( g => {
const u = g . util ?? 0 , C = 2 * Math . PI * 50 , col = u > 85 ? css ( '--ok' ) : u > 50 ? css ( '--accent' ) : css ( '--warn' );
2026-10-18 06:34:21 +08:00
const memP = 100 * g . mem_used / BUD , tc = g . temp > 83 ? css ( '--bad' ) : g . temp > 75 ? css ( '--warn' ) : css ( '--c6' );
2026-10-17 17:15:16 +00:00
const row = ( k , v , p , c ) => `<div class="gr"><div class="l"><span> ${ k } </span><b> ${ v } </b></div><div class="mini"><div style="width: ${ Math . min ( 100 , p ) } %;background: ${ c } "></div></div></div>` ;
return `<div class="gpuwrap"><div class="ring"><svg width="116" height="116"><circle cx="58" cy="58" r="50" stroke="#1b2433" stroke-width="10" fill="none"/>
<circle cx="58" cy="58" r="50" stroke=" ${ col } " stroke-width="10" fill="none" stroke-linecap="round" stroke-dasharray=" ${ C } " stroke-dashoffset=" ${ C * ( 1 - u / 100 ) } " style="transition:stroke-dashoffset .6s"/></svg>
<div class="c"><div><b> ${ u } %</b><span>利用率</span></div></div></div>
2026-10-18 06:34:21 +08:00
<div class="gpurows"> ${ row ( '显存' , ` ${ ( g . mem_used / 1024 ). toFixed ( 1 ) } / ${ ( BUD / 1024 ). toFixed ( 0 ) } GB 预算(整卡 ${ ( g . mem_total / 1024 ). toFixed ( 0 ) } ) `, memP , memP > 100 ? css ( '--bad' ) : memP > 85 ? css ( '--warn' ) : css ( '--accent2' )) }
2026-10-17 17:15:16 +00:00
${ row ( '温度' , ` ${ g . temp } °C` , g . temp , tc ) }${ row ( '功耗' , g . power != null ? ` ${ Math . round ( g . power ) } W` : '—' , g . power ? g . power / 3.5 : 0 , css ( '--c7' )) } </div></div>
<div class="gname"> ${ g . name } </div>` ;
}). join ( '<hr style="border:none;border-top:1px solid var(--line);margin:14px 0">' ) : empty ( '未检测到 nvidia-smi' );
// 数据
const D = s . data || {}, st = D . stats , ix = D . index ;
const row = ( k , v , cls = '' ) => `<tr><td> ${ k } </td><td class="n ${ cls } "> ${ v } </td></tr>` ;
if ( D . consistency == null && ! st && ! ix && ! D . gen_tasks_written ) $ ( 'data' ). innerHTML = empty ( '还没开始生成数据' );
else {
let h = '<table>' ;
h += row ( '老师一致性' , D . consistency == null ? '—' : pct ( D . consistency ), D . consistency == null ? '' : D . consistency >= 0.9 ? 'ok' : 'warn' );
h += row ( '已生成任务' , D . gen_tasks_written ? ` ${ D . gen_tasks_written . toLocaleString () } 条 · ${ D . shards } 个分片` : '—' );
if ( st ) {
h += row ( '保留率' , fmt ( st . keepRate ), st . keepRate >= 0.75 ? 'ok' : 'bad' );
if ( st . gapDrop != null ) h += row ( `间隙 < ${ st . minGapFilterMm ?? 3 } mm 剔除` , ` ${ st . gapDrop . toLocaleString () } ( ${ pct ( st . gapDrop / Math . max ( 1 , st . keep + st . gapDrop )) } ) `, st . gapDrop / Math . max ( 1 , st . keep + st . gapDrop ) > 0.25 ? 'bad' : '' );
h += row ( '样本数(每 3 步)' , ( st . samplesEvery3 || 0 ). toLocaleString (), st . samplesEvery3 >= 1.7 e6 ? 'ok' : 'warn' );
if ( st . consensusDrop != null ) h += row ( '共识丢弃' , st . consensusDrop . toLocaleString ());
if ( st . vertexMinGapMm ) h += row ( '最小间隙 p10 / p50 / p90' , ` ${ st . vertexMinGapMm . p10 ?? '—' } / ${ st . vertexMinGapMm . p50 } / ${ st . vertexMinGapMm . p90 } mm` );
}
if ( ix ) h += row ( '训练 / 验证样本' , ` ${ ix . train_samples . toLocaleString () } / ${ ix . val_samples . toLocaleString () } ` );
if ( D . r2 ) h += row ( 'R2 近邻标签 std' , D . r2 . pass == null ? '未判定' : ` ${ fmt ( D . r2 . label_std_mean ) } ( < 0.10) `, D . r2 . pass == null ? 'dim' : D . r2 . pass ? 'ok' : 'bad' );
$ ( 'data' ). innerHTML = h + '</table>' ;
}
$ ( 'm5' ). innerHTML = s . m5 && s . m5 . length ? '<table><tr><th>动作头</th><th class="n">纯策略穿柱</th><th class="n">到达</th><th class="n">加安全层穿柱</th><th class="n">结论</th></tr>' + s . m5 . map ( m =>
`<tr><td> ${ m . head } </td><td class="n ${ m . policy_only . collision_rate <= 0.02 ? 'ok' : 'bad' } "> ${ pct ( m . policy_only . collision_rate , 2 ) } </td><td class="n"> ${ pct ( m . policy_only . success ) } </td><td class="n"> ${ pct ( m . with_sim_safety . collision_rate , 2 ) } </td><td class="n"><span class="chip ${ m . pass ? 'ok' : 'bad' } "> ${ m . pass ? '通过' : '不通过' } </span></td></tr>` ). join ( '' )
+ `</table><div class="dim" style="margin-top:8px;font-size:12px">选定模型:<span style="font-family:var(--mono)"> ${ s . selected || '—' } </span></div>` : empty ( '预训练完成后运行 m5' );
$ ( 'mine' ). innerHTML = s . mine && s . mine . length ? '<table><tr><th>轮</th><th class="n">E1 撞</th><th class="n">E2 极小</th><th class="n">正确</th><th class="n">提升</th><th class="n">R7 左右</th><th class="n">门禁</th></tr>' + s . mine . map ( m => {
const r = m . report || {}, g = m . gate || {};
const chip = g . accept == null ? '<span class="chip dim">进行中</span>' : g . accept ? '<span class="chip ok">接受</span>' : '<span class="chip bad">回滚</span>' ;
return `<tr><td> ${ m . round } </td><td class="n"> ${ pct ( r . E1 , 2 ) } </td><td class="n"> ${ pct ( r . E2 , 2 ) } </td><td class="n"> ${ pct ( r . ok ) } </td><td class="n"> ${ isNum ( g . gain_pp ) ? '+' + g . gain_pp . toFixed ( 1 ) + 'pp' : '—' } </td><td class="n"> ${ isNum ( g . R7_left_share_shift_pp ) ? g . R7_left_share_shift_pp . toFixed ( 1 ) + 'pp' : '—' } </td><td class="n"> ${ chip } </td></tr>` ;
}). join ( '' ) + '</table>' : empty ( 'APG 之后开始挖错' );
$ ( 'bench' ). innerHTML = s . bench ? '<table><tr><th>集合</th><th class="n">通过</th><th class="n">穿模帧</th><th class="n">刹停率</th><th class="n">推理 P99</th></tr>' + Object . entries ( s . bench ). map (([ k , v ]) =>
`<tr><td> ${ k } </td><td class="n ${ v . ok === v . n ? 'ok' : 'bad' } "> ${ v . ok } / ${ v . n } </td><td class="n ${ v . collisionFrames ? 'bad' : 'ok' } "> ${ v . collisionFrames } </td><td class="n"> ${ pct ( v . brakeRate , 2 ) } </td><td class="n"> ${ v . inferMsP99 ?? '—' } ms</td></tr>` ). join ( '' ) + '</table>' : empty ( '导出 ONNX 后运行 bench' );
const A = s . acceptance ;
$ ( 'accept' ). innerHTML = A ? `<div class="accbanner ${ A . all_pass ? 'ok' : 'bad' } "> ${ A . all_pass ? '✓ 全部通过' : `✕ ${ A . failed . length } 项未通过` } </div><table>` + A . items . map ( i =>
`<tr><td> ${ i . item } </td><td class="n"> ${ typeof i . value === 'number' ? ( /率|比例/ . test ( i . item ) && i . value <= 1 ? pct ( i . value , 1 ) : fmt ( i . value )) : ( Array . isArray ( i . value ) || i . value == null ? '' : i . value ) } </td><td class="n"><span class="chip ${ i . pass === true ? 'ok' : i . pass === false ? 'bad' : 'dim' } "> ${ i . pass === true ? '通过' : i . pass === false ? '未过' : '—' } </span></td></tr>` ). join ( '' ) + '</table>' : empty ( '最后一步 accept' );
$ ( 'foot' ). textContent = ` ${ DEMO ? '示例数据 · ' : '' } 每 5 秒刷新 · 网页可随时关闭,训练不受影响 · ${ new Date (). toLocaleTimeString ( 'zh-CN' , { hour12 : false } )}` ;
}
/* ===================== 全流程卡片 / 开始停止 / 终端 ===================== */
function renderPipe ( s ) {
const P = s . pipeline , running = job && job . running ;
2026-10-17 18:57:48 +00:00
const ck = s . check || {}, lk = s . lock ;
const ckline = ck . pending ? '<div class="ckline">环境自检中…</div>'
: ck . problems && ck . problems . length ? `<div class="ckline bad">✕ ${ ck . problems . join ( '; ' ) } </div>`
: ck . python ? `<div class="ckline">Python ${ ck . python } · torch ${ ck . torch }${ ck . gpu ? ' · ' + ck . gpu : '' }${ ck . warn ? ` · <span class="warn"> ${ ck . warn } </span>` : '' } </div>` : '' ;
const who = job && job . external ? `由 ${ ( lk && lk . owner || '' ). startsWith ( '网页' ) ? '上一个控制台窗口' : '终端' } 启动(pid ${ lk && lk . pid } , ${ Math . round ( lk && lk . age_s || 0 ) } 秒前心跳)` : job && job . pid ? `PID ${ job . pid } ` : '' ;
const btns = ` ${ ckline } <div class="ctlrow"> ${ running || ( P && P . current )
? `<button class="danger" onclick="stopJob()">■ 停止</button><span class="hint"> ${ who } </span>`
: `<button class="primary" onclick="openStart()" ${ ck . problems && ck . problems . length ? 'disabled' : '' } >▶ ${ P ? '继续训练' : '开始训练' } </button><span class="hint"> ${ job && job . rc != null ? ( job . stopped ? '上次已手动停止,再点即从停下的那步继续' : job . rc === 0 ? '上次运行已全部完成' : `上次运行失败(返回码 ${ job . rc } ),看终端输出;修好后再点即续跑` ) : '已完成的步骤会自动跳过' } </span>` } </div>` ;
2026-10-17 17:15:16 +00:00
if ( ! P ) { $ ( 'pipe' ). innerHTML = `<h2><span>全流程</span><span class="r">尚未开始</span></h2> ${ empty ( '还没运行过流水线。点「开始训练」从对拍一直跑到验收;中途每一步的进度都显示在这里。' ) }${ btns } ` ; return ; }
const plan = P . plan && P . plan . length ? P . plan : Object . keys ( P . stages );
let tot = 0 , got = 0 ;
for ( const k of plan ) { const w = SM [ k ] ? . w || 0.1 , r = P . stages [ k ]; tot += w ; if ( r && r . rc === 0 && P . current !== k ) got += w ; else if ( P . current === k ) got += w * (( P . progress && P . progress . frac ) || 0 ); }
const frac = tot ? got / tot : 0 , now = Date . now () / 1000 ;
const cur = P . current , cr = cur && P . stages [ cur ], pr = P . progress || {};
const doneN = plan . filter ( k => P . stages [ k ] && P . stages [ k ]. rc === 0 && P . current !== k ). length ;
const failed = plan . find ( k => P . stages [ k ] && P . stages [ k ]. rc != null && P . stages [ k ]. rc !== 0 );
const allDone = ! cur && doneN === plan . length ;
let remain = 0 ; for ( const k of plan ) { const r = P . stages [ k ]; if ( ! r || r . rc == null ) remain += ( SM [ k ] ? . w || 0.1 ) * ( k === cur ? 1 - ( pr . frac || 0 ) : 1 ); }
const head = cur ? `<div class="now">正在运行<b> ${ SM [ cur ] ? . n || cur } </b> ${ pr . label || '' } </div>`
: allDone ? `<div class="now">状态<b class="ok">全部完成</b>共 ${ plan . length } 步</div>`
2026-10-17 18:57:48 +00:00
: failed ? `<div class="now">状态<b class="bad">停在「 ${ SM [ failed ] ? . n || failed } 」</b> ${ ( P . stages [ failed ]. info || { } ).timed_out || P.stages[failed].rc === 124 ? '超过时间上限,判定卡死' : P.stages[failed].rc === -15 || P.stages[failed].rc === 143 ? '已停止' : '失败,看终端输出'}</div>`
2026-10-17 17:15:16 +00:00
: `<div class="now">状态<b>未在运行</b>已完成 ${ doneN } / ${ plan . length } 步</div>` ;
$ ( 'pipe' ). innerHTML = `<h2><span>全流程</span><span class="r"> ${ doneN } / ${ plan . length } 步完成</span></h2>
<div class="overall"> ${ head } <div class="bigpct"> ${ ( 100 * frac ). toFixed ( 1 ) } <small>%</small></div></div>
<div class="bar ${ cur ? '' : 'idle' } "><div class="fill" style="width: ${ 100 * frac } %"></div></div>
${ cur ? `<div class="sublabel"><span>当前步骤 · ${ SM [ cur ] ? . n } </span><span> ${ pr . frac != null ? pct ( pr . frac ) : '无法计量,按用时估' } </span></div>
<div class="bar thin ${ pr . frac == null ? 'ind' : '' } "><div class="fill" style="width: ${ pr . frac != null ? 100 * pr . frac : 30 } %"></div></div>` : '' }
<div class="facts">
<div class="fact"><div class="k">总用时</div><div class="v"> ${ hms (( cur ? now : Math . max (... Object . values ( P . stages ). map ( r => r . end || r . start ))) - P . started ) } </div></div>
<div class="fact"><div class="k">本步用时</div><div class="v"> ${ cr ? hms ( now - cr . start ) : '—' } </div></div>
2026-10-17 18:57:48 +00:00
<div class="fact"><div class="k">本步粗估 / 上限</div><div class="v"> ${ cur ? hmsShort (( SM [ cur ] ? . w || 0 ) * 3600 ) + ' / ' + hmsShort (( cr && cr . timeout_h || SM [ cur ] ? . to || 0 ) * 3600 ) : '—' } </div></div>
2026-10-17 17:15:16 +00:00
<div class="fact"><div class="k">全流程剩余(粗估)</div><div class="v"> ${ allDone ? '—' : '约 ' + hmsShort ( remain * 3600 ) } </div></div>
</div> ${ btns } ` ;
}
function renderJob () {
const busy = ( job && job . running ) || ( lastStatus && lastStatus . pipeline && lastStatus . pipeline . current );
$ ( 'btnStart' ). style . display = busy ? 'none' : '' ; $ ( 'btnStop' ). style . display = busy ? '' : 'none' ;
}
async function pollConsole () {
const d = await api ( `/api/console?offset= ${ conOff } ` ). catch (() => null ); if ( ! d ) return ;
const t = $ ( 'term' ), atBottom = t . scrollTop + t . clientHeight >= t . scrollHeight - 30 ;
if ( d . reset ) t . innerHTML = '' ;
if ( d . text ) {
const esc = x => x . replace ( /&/g , '&' ). replace ( /</g , '<' );
t . insertAdjacentHTML ( 'beforeend' , d . text . split ( '\n' ). map ( l => / error | Error | 失败 | Traceback | 不通过 | Terminated / . test ( l ) ? `<span class="e"> ${ esc ( l ) } </span>` : /^== |^\$ / . test ( l ) ? `<span class="c"> ${ esc ( l ) } </span>` : /PASS|通过|完成/ . test ( l ) ? `<span class="g"> ${ esc ( l ) } </span>` : esc ( l )). join ( '\n' ));
if ( t . textContent . length > 400000 ) t . textContent = t . textContent . slice ( - 200000 );
}
if ( ! t . textContent ) t . innerHTML = '<span style="color:var(--dim2)">点「开始训练」后,这里实时显示命令行输出。</span>' ;
conOff = d . offset ; if ( atBottom || d . reset ) t . scrollTop = t . scrollHeight ;
}
function toast ( msg ) { const t = $ ( 'toast' ); t . textContent = msg ; t . style . display = 'block' ; clearTimeout ( toast . h ); toast . h = setTimeout (() => t . style . display = 'none' , 3500 ); }
function openStart () {
2026-10-17 18:57:48 +00:00
const P = lastStatus && lastStatus . pipeline , ck = lastStatus && lastStatus . check ;
if ( ck && ck . problems && ck . problems . length ) { toast ( '环境自检未通过:' + ck . problems . join ( '; ' )); return ; }
const w = $ ( 'fWarn' ); const upd = () => { const msgs = [];
const done = P ? ( P . plan || []). filter ( k => P . stages [ k ] && P . stages [ k ]. rc === 0 ) : [];
if ( $ ( 'fMode' ). value === 'resume' && done . length ) msgs . push ( `将跳过已完成的 ${ done . length } 步: ${ done . map ( k => SM [ k ] ? . n || k ). join ( '、' ) } 。改了参数会自动改为从头重跑。` );
if ( $ ( 'fMode' ). value === 'fresh' && P ) msgs . push ( '从头重跑:上次的数据、模型、评测结果整体移到 ml/archive/<时间>/(不删除)。' );
2026-10-17 17:15:16 +00:00
w . style . display = msgs . length ? 'block' : 'none' ; w . innerHTML = msgs . join ( '<br>' ); };
2026-10-17 18:57:48 +00:00
$ ( 'fMode' ). onchange = upd ; upd ();
2026-10-17 17:15:16 +00:00
$ ( 'modal' ). classList . add ( 'show' );
}
async function startJob () {
2026-10-17 18:57:48 +00:00
const env = { CFG : $ ( 'fCfg' ). value , CONSENSUS : $ ( 'fCons' ). value , N_TASKS : $ ( 'fN' ). value . trim (), NPROC : $ ( 'fP' ). value . trim (), TARGET_SAMPLES : $ ( 'fT' ). value . trim (), ROUNDS : $ ( 'fR' ). value . trim (), MIN_GAP_MM : $ ( 'fG' ). value . trim () };
const body = JSON . stringify ({ fresh : $ ( 'fMode' ). value === 'fresh' , env });
const r = DEMO ? await api ( '/api/start' , body ) : await post ( '/api/start' , body );
2026-10-17 17:15:16 +00:00
if ( r . error ) { toast ( '没能开始:' + r . error ); return ; }
$ ( 'modal' ). classList . remove ( 'show' ); toast ( '已开始' ); conOff = 0 ; userPicked = false ; poll ();
}
async function stopJob () {
2026-10-17 18:57:48 +00:00
if ( ! confirm ( '确定停止?训练会先存档再退出(最多等 90 秒),生成/打包按已写完的部分保留。之后点「续跑」从断点接着来。' )) return ;
toast ( '正在停止…训练阶段会先存档' );
const r = DEMO ? await api ( '/api/stop' , '{}' ) : await post ( '/api/stop' );
2026-10-17 17:15:16 +00:00
toast ( r . error ? '没能停止:' + r . error : '已停止' ); poll ();
}
$ ( 'btnStart' ). onclick = openStart ; $ ( 'btnStop' ). onclick = stopJob ;
$ ( 'fCancel' ). onclick = () => $ ( 'modal' ). classList . remove ( 'show' ); $ ( 'fGo' ). onclick = startJob ;
$ ( 'modal' ). onclick = e => { if ( e . target === $ ( 'modal' )) $ ( 'modal' ). classList . remove ( 'show' ); };
/* ===================== 控件 ===================== */
$ ( 'smooth' ). oninput = () => { $ ( 'smoothv' ). textContent = smoothv (). toFixed ( 2 ); render (); };
$ ( 'logy' ). onclick = () => { logyOn = ! logyOn ; $ ( 'logy' ). classList . toggle ( 'on' , logyOn ); render (); };
$ ( 'run' ). onchange = () => { userPicked = true ; cur = null ; poll (); };
$ ( 'scenario' ). onchange = () => { api . scenario ( $ ( 'scenario' ). value ); cur = null ; userPicked = false ; conOff = 0 ; $ ( 'run' ). innerHTML = '' ; poll (); };
$ ( 'auto' ). onclick = () => { const on = ! $ ( 'auto' ). classList . contains ( 'on' ); $ ( 'auto' ). classList . toggle ( 'on' , on ); clearInterval ( timer ); if ( on ) timer = setInterval ( poll , 5000 ); };
timer = setInterval ( poll , 5000 ); poll ();
/* =====================================================================
示例数据:两种场景,每次刷新让"运行中"的那条日志向前推进。
===================================================================== */
function demoApi () {
let seed = 7 ; const rnd = () => ( seed = ( seed * 16807 ) % 2147483647 ) / 2147483647 ;
const gauss = () => { let u = 0 ; for ( let i = 0 ; i < 6 ; i ++ ) u += rnd (); return ( u - 3 ) * 1.41 ; };
const now = () => Date . now () / 1000 ;
const T0 = now () - 2.05 * 3600 ;
function trainRec ( st , total , sps , t0 , wta ) {
const lr = 1e-3 * ( st < 1000 ? st / 1000 : 0.5 * ( 1 + Math . cos ( Math . PI * ( st - 1000 ) / ( total - 1000 ))));
const mf = ( wta ? 0.42 : 0.55 ) * Math . exp ( - st / 9000 ) + 0.12 + 0.012 * gauss (), fk = 2.6 * Math . exp ( - st / 6000 ) + 0.35 + 0.04 * gauss ();
const sm = 3.2 * Math . exp ( - st / 4000 ) + 0.6 + 0.06 * gauss (), val = 0.4 * Math . exp ( - st / 5000 ) + 0.05 + 0.006 * gauss (), sub = 0.25 * Math . exp ( - st / 7000 ) + 0.04 + 0.004 * gauss ();
const sp = sps * ( 1 - 0.18 * Math . exp ( - st / 250 )) * ( 1 + 0.012 * gauss ());
return { step : st , total , t : t0 + st * 1024 / sps , loss : mf + 0.5 * fk + 0.05 * sm + 0.1 * val + 0.1 * sub , mf , fk_cm : fk , smooth : sm , value : val , sub ,
gnorm : Math . max ( 0.15 , 0.55 + 2.4 * Math . exp ( - st / 2500 ) + 0.12 * gauss ()), lr , samples_per_s : sp , eta_h : ( total - st ) * 1024 / sps / 3600 };
}
function trainEval ( x , t0 , sps ) {
const q = 1 - Math . exp ( - x / 14000 );
return { eval : { step : x , t : t0 + x * 1024 / sps , n : 512 , n_ref : 512 , success : Math . min ( 0.975 , 0.08 + 0.87 * q + 0.012 * gauss ()), collision_rate : Math . max ( 0.004 , 0.31 * ( 1 - q ) + 0.012 + 0.005 * gauss ()),
time_ok_1p3 : Math . min ( 0.95 , 0.05 + 0.8 * q + 0.012 * gauss ()), start_fail : Math . max ( 0 , 0.05 * ( 1 - q ) + 0.003 * gauss ()) } };
}
function trainLog ( total , from , to , sps , t0 , evalEvery = 2000 , wta = false ) {
const L = [];
for ( let st = from ; st <= to ; st += 50 ) { L . push ( trainRec ( st , total , sps , t0 , wta )); if ( evalEvery && st > 0 && st % evalEvery === 0 ) L . push ( trainEval ( st , t0 , sps )); }
return L ;
}
function apgRec ( it , total , t0 ) {
const q = 1 - Math . exp ( - it / 900 );
return { it , total , t : t0 + it * 3.1 , plans : 60 + Math . floor ( rnd () * 45 ), err_mm_mean : Math . max ( 3 , 140 * ( 1 - q ) + 9 + 5 * gauss ()), reach_frac : Math . min ( 1 , 0.35 + 0.52 * q + 0.03 * gauss ()),
bc : 0.9 - 0.15 * q + 0.02 * gauss (), gnorm : Math . max ( 0.2 , 1.2 + 0.25 * gauss ()), col : Math . max ( 0 , 2.4 * ( 1 - q ) + 0.15 + 0.06 * gauss ()), tgt : Math . max ( 0 , 3.5 * ( 1 - q ) + 0.2 + 0.08 * gauss ()), smooth : 40 - 12 * q + 1.5 * gauss () };
}
function apgLog ( total , from , to , t0 , base = 0.93 ) {
const L = [];
for ( let it = from ; it <= to ; it += 10 ) { L . push ( apgRec ( it , total , t0 )); if ( it > 0 && it % 500 === 0 ) { const q = 1 - Math . exp ( - it / 900 ); L . push ({ eval : { it , success : base + 0.035 * q + 0.004 * gauss (), collision_rate : 0.028 * ( 1 - q ) + 0.006 , time_ok_1p3 : 0.86 + 0.06 * q , start_fail : 0.001 } }); } }
return L ;
}
let sc , runs , logs , pipe , jobS , con , genAt = 0 ;
// 示例流水线:stages 里的 [阶段, 开始距今小时, 用时小时]
const mkPipe = ( done , cur , curStartH ) => { const t = now (); const st = {}; let tt = t - curStartH * 3600 ;
for ( let i = done . length - 1 ; i >= 0 ; i -- ) { const [ k , dur ] = done [ i ]; st [ k ] = { start : tt - dur * 3600 , end : tt , rc : 0 , info : {} }; tt -= dur * 3600 ; }
if ( cur ) st [ cur ] = { start : t - curStartH * 3600 , end : null , rc : null , info : {} };
2026-10-17 18:57:48 +00:00
return { started : tt , plan : [ 'kin' , 'r4' , 'gate1' , 'gen' , 'prep' , 'train' , 'm5' , 'apg' , 'mine' , 'export' , 'bench' , 'accept' ], stages : st , current : cur }; };
2026-10-17 17:15:16 +00:00
const CON = {
2026-10-17 18:57:48 +00:00
train : '$ FROM=kin TO=accept bash ml/pipeline.sh all\n计划:kin r4 gate1 gen prep train m5 apg mine export bench accept\n== node ml/prep/export_kin.cjs\n== node ml/eval/dump_parity.cjs 2000\n== python -m ml.eval.check_parity\n{"PASS": true, "fk_mm": 0.0004, "gap_mm": 0.0011}\n== node ml/gen/consistency.cjs 300 4242\n{"consistentByGateRule": 0.86}\n一致性门禁结果 → CONSENSUS=1\n== bash ml/gen/run_parallel.sh 1786 28 1 --consensus 4\n样本数 1702000 ≥ 目标,完成\n== bash ml/prep/run_prep.sh 28\n== python -m ml.eval.check_r2 --samples ml/data/samples --out ml/data/r2.json\n{"pairs": 250, "label_std_mean": 0.071, "pass": true}\n== python -m ml.train --samples ml/data/samples --out ml/runs/bench_base --steps 2000 --no-eval\nstep 2000 loss 1.912 6150 samples/s\n== python -m ml.train --samples ml/data/samples --out ml/runs/base\n' ,
gen : '$ FROM=kin TO=accept bash ml/pipeline.sh all\n计划:kin r4 gate1 gen prep train m5 apg mine export bench accept\n== node ml/prep/export_kin.cjs\n== python -m ml.eval.check_parity\n{"PASS": true}\n== node ml/gen/consistency.cjs 300 4242\n{"consistentByGateRule": 0.86}\n一致性门禁结果 → CONSENSUS=1\n== bash ml/gen/run_parallel.sh 1786 28 1 --consensus 4\n' ,
2026-10-17 17:15:16 +00:00
late : '... 挖错第 2 轮\nround 2 gain 1.1 pp\n== python -m ml.mining.mine_round --ckpt ml/runs/current.pt --seed 1003 --n 50000 --out ml/runs/mine/r3\n{"n": 50000, "E1": 0.009, "E2": 0.019, "ok": 0.938}\n== python -m ml.apg.apg_finetune --init ml/runs/current.pt --out ml/runs/apg_r3 --steps 1000\n' , idle : '' };
const GPU = () => [{ name : 'NVIDIA GeForce RTX 3090' , util : 93 + Math . round ( 5 * rnd ()), mem_used : 15872 + Math . round ( 500 * rnd ()), mem_total : 24576 , temp : 71 + Math . round ( 3 * rnd ()), power : 318 + Math . round ( 24 * rnd ()) }];
const DATA = { kin_pass : true , consistency : 0.86 , gen_tasks_written : 56412 , shards : 56 ,
stats : { tasks : 56412 , keep : 43120 , gapDrop : 4830 , minGapFilterMm : 3 , keepRate : 0.85 , samplesEvery3 : 1702000 , samplesPerTask : 36.5 , consensusDrop : 7520 , vertexMinGapMm : { p10 : 3.1 , p50 : 4.4 , p90 : 5.9 , max : 12.3 } },
index : { train_samples : 1617000 , val_samples : 85000 }, r2 : { label_std_mean : 0.071 , pass : true } };
function setup ( name ) {
sc = name ; seed = 7 ; con = CON [ name ] || '' ; jobS = { running : name !== 'idle' , pid : 21844 , from : 'kin' , to : 'accept' , started : now () - 5 * 3600 , rc : null , bash : true }; genAt = 31200 ;
if ( name === 'idle' ) { runs = {}; logs = {}; pipe = null ; jobS = { running : false , bash : true }; return ; }
2026-10-17 18:57:48 +00:00
if ( name === 'gen' ) { runs = {}; logs = {}; pipe = mkPipe ([[ 'kin' , 0.02 ], [ 'r4' , 0.05 ], [ 'gate1' , 0.47 ]], 'gen' , 2.7 ); pipe . stages . gen . info . target_tasks = 50008 ; return ; }
if ( name === 'train' ) { pipe = mkPipe ([[ 'kin' , 0.02 ], [ 'r4' , 0.05 ], [ 'gate1' , 0.47 ], [ 'gen' , 4.6 ], [ 'prep' , 0.38 ]], 'train' , 2.15 );
2026-10-17 17:15:16 +00:00
runs = { base : { kind : 'train' , at : 36000 , total : 60000 , sps : 6208 , t0 : T0 , live : true },
bench_base : { kind : 'bench' , at : 2000 , total : 2000 , sps : 6150 , t0 : T0 - 700 } };
logs = { base : trainLog ( 60000 , 0 , 36000 , 6208 , T0 ), bench_base : trainLog ( 2000 , 0 , 2000 , 6150 , T0 - 700 , 0 ) };
2026-10-17 18:57:48 +00:00
} else { pipe = mkPipe ([[ 'kin' , 0.02 ], [ 'r4' , 0.05 ], [ 'gate1' , 0.47 ], [ 'gen' , 4.6 ], [ 'prep' , 0.38 ], [ 'train' , 2.9 ], [ 'm5' , 0.2 ], [ 'apg' , 2.6 ]], 'mine' , 3.4 ); pipe . stages . mine . info . rounds = 6 ;
2026-10-17 17:15:16 +00:00
pipe . stages . export = pipe . stages . bench = pipe . stages . accept = undefined ; delete pipe . stages . export ; delete pipe . stages . bench ; delete pipe . stages . accept ;
const t1 = now () - 9 * 3600 ;
runs = { base : { kind : 'train' , at : 60000 , total : 60000 , sps : 6208 , t0 : t1 - 3 * 3600 },
bench_base : { kind : 'bench' , at : 2000 , total : 2000 , sps : 6150 , t0 : t1 - 3.2 * 3600 },
apg : { kind : 'apg' , at : 3000 , total : 3000 , t0 : t1 },
apg_r3 : { kind : 'apg' , at : 620 , total : 1000 , t0 : now () - 620 * 3.1 , live : true } };
logs = { base : trainLog ( 60000 , 0 , 60000 , 6208 , t1 - 3 * 3600 ), bench_base : trainLog ( 2000 , 0 , 2000 , 6150 , t1 - 3.2 * 3600 , 0 ),
apg : apgLog ( 3000 , 0 , 3000 , t1 ), apg_r3 : apgLog ( 1000 , 0 , 620 , now () - 620 * 3.1 , 0.955 ) };
}
}
setup ( 'train' );
const mtime = name => { const r = runs [ name ]; const L = logs [ name ]; const last = L [ L . length - 1 ]; return r . live ? now () - 2 : ( last . t || last . eval ? . t || now () - 3600 ); };
const status = () => {
let P = pipe && JSON . parse ( JSON . stringify ( pipe ));
if ( P && P . current ) {
const r = sc === 'train' ? runs . base : runs . apg_r3 ;
P . progress = ! [ 'gen' , 'train' , 'late' ]. includes ( sc ) || ( sc !== 'gen' && ! ( sc === 'train' ? runs . base : runs . apg_r3 )) ? { frac : null , label : '进行中(示例)' } : sc === 'gen' ? { frac : Math . min ( 1 , ( genAt += 180 ) / 50008 ), label : `已生成 ${ genAt . toLocaleString () } / 50,008 条任务` }
: sc === 'train' ? { frac : ( 2000 + r . at ) / 62000 , label : `正式训练 ${ r . at . toLocaleString () } / 60,000 步` , run : 'base' }
: { frac : ( 2 + 0.3 + 0.6 * r . at / r . total ) / 6 , label : `第 3 轮:APG ${ r . at } / 1,000 步(共最多 6 轮,提升不足会提前停)` , run : 'apg_r3' };
}
const gpuIdle = [{ name : 'NVIDIA GeForce RTX 3090' , util : 0 , mem_used : 412 , mem_total : 24576 , temp : 38 , power : 24 }];
const base = { now : now (), gpu : sc === 'train' || sc === 'late' ? GPU () : gpuIdle , pipeline : P , data : DATA , m5 : [], selected : null , mine : [], bench : null , acceptance : null };
if ( sc === 'idle' ) return { ... base , data : {} };
if ( sc === 'gen' ) return { ... base , data : { kin_pass : true , consistency : 0.86 , gen_tasks_written : genAt , shards : 28 , stats : null , index : null , r2 : null } };
if ( sc === 'train' ) return base ;
return { ... base ,
m5 : [{ head : 'base' , pass : true , policy_only : { collision_rate : 0.012 , success : 0.912 }, with_sim_safety : { collision_rate : 0.0 , success : 0.948 } }],
selected : 'ml/runs/base/best.pt' ,
mine : [{ round : 'r1' , report : { E1 : 0.018 , E2 : 0.031 , ok : 0.902 }, gate : { gain_pp : 2.4 , R7_left_share_shift_pp : 1.2 , accept : true } },
{ round : 'r2' , report : { E1 : 0.011 , E2 : 0.022 , ok : 0.931 }, gate : { gain_pp : 1.1 , R7_left_share_shift_pp : 0.8 , accept : true } },
{ round : 'r3' , report : { E1 : 0.009 , E2 : 0.019 , ok : 0.938 }, gate : null }],
bench : { nc : { n : 30 , ok : 30 , collisionFrames : 0 , brakeRate : 0.004 , inferMsP99 : 9.8 }, route : { n : 40 , ok : 40 , collisionFrames : 0 , brakeRate : 0.002 , inferMsP99 : 9.6 },
r8 : { n : 50 , ok : 48 , collisionFrames : 0 , brakeRate : 0.031 , inferMsP99 : 10.1 }, nc1000 : { n : 1000 , ok : 997 , collisionFrames : 0 , brakeRate : 0.006 , inferMsP99 : 9.9 } },
acceptance : { all_pass : false , failed : [ 'R8 恢复率' ], items : [
{ item : 'held-out 可达任务到达率' , value : 0.983 , pass : true }, { item : '起步失败率(R2) ' , value : 0.0008 , pass : true },
{ item : 'D 类停靠距离 ≤ 老师 + 5 mm 的比例' , value : 0.991 , pass : true }, { item : '部署形态穿模帧' , value : 0 , pass : true },
{ item : 'near_column 30/30' , value : '30/30' , pass : true }, { item : 'R7 左右比例变化' , value : '1.2pp' , pass : true },
{ item : 'R8 恢复率' , value : 0.96 , pass : false }, { item : 'R10 训练吞吐(样本/s) ' , value : 6208 , pass : true , rule : '样本/s' } ] } };
};
const fn = async ( path , body ) => {
const u = new URL ( path , 'http://x' );
if ( u . pathname === '/api/runs' ) return Object . entries ( runs ). map (([ name , r ]) => ({ name , kind : r . kind , size : 0 , mtime : mtime ( name ), has_best : true , diverged : false })). sort (( a , b ) => b . mtime - a . mtime );
if ( u . pathname === '/api/log' ) {
const name = u . searchParams . get ( 'run' ), off = + u . searchParams . get ( 'offset' ), r = runs [ name ];
if ( ! r ) return { records : [], offset : 0 , reset : true , mtime : now () };
if ( off === 0 ) return { records : logs [ name ], offset : logs [ name ]. length , reset : true , mtime : mtime ( name ), live : !! r . live };
if ( r . live && r . at < r . total ) { // 模拟推进
const step = r . kind === 'apg' ? 10 : 250 , from = r . at + ( r . kind === 'apg' ? 10 : 50 ); r . at = Math . min ( r . total , r . at + step );
const add = r . kind === 'apg' ? apgLog ( r . total , from , r . at , r . t0 , 0.955 ) : trainLog ( r . total , from , r . at , r . sps , r . t0 );
if ( r . kind !== 'apg' ) add . forEach ( v => { if ( v . t ) v . t = now (); });
logs [ name ]. push (... add ); return { records : add , offset : off + add . length , reset : false , mtime : now (), live : true };
}
return { records : [], offset : off , reset : false , mtime : mtime ( name ), live : !! r . live };
}
if ( u . pathname === '/api/status' ) return status ();
if ( u . pathname === '/api/job' ) return jobS ;
if ( u . pathname === '/api/console' ) { const off = + u . searchParams . get ( 'offset' ); return { text : con . slice ( off ), offset : con . length , reset : off === 0 || off > con . length }; }
2026-10-17 18:57:48 +00:00
if ( u . pathname === '/api/start' ) { const b = JSON . parse ( body || '{}' ); b . from = b . from || 'kin' ; b . to = b . to || 'accept' ; jobS = { running : true , pid : 23001 , from : b . from , to : b . to , started : now (), rc : null , bash : true };
con = `$ ${ b . fresh ? 'FRESH=1 ' : '' } bash ml/pipeline.sh all\n(示例数据:真实使用时这里是命令行输出)\n` ;
2026-10-17 17:15:16 +00:00
if ( ! pipe ) pipe = mkPipe ([], b . from , 0.001 ); else { pipe . current = b . from ; pipe . stages [ b . from ] = { start : now (), end : null , rc : null , info : {} }; }
return { ok : true , pid : 23001 }; }
if ( u . pathname === '/api/stop' ) { jobS = { ... jobS , running : false , rc : - 15 , stopped : true };
if ( pipe && pipe . current ) { pipe . stages [ pipe . current ]. end = now (); pipe . stages [ pipe . current ]. rc = - 15 ; pipe . current = null ; }
Object . values ( runs ). forEach ( r => r . live = false ); con += '\n[已在网页上停止]\n' ; return { ok : true }; }
};
fn . scenario = setup ;
return fn ;
}
</ script >
</ body >
</ html >