dsh-visualization 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +76 -0
- package/README.md +262 -2
- package/build.cjs +20 -0
- package/client.js +96 -0
- package/cordis.patch.yml +3 -0
- package/fold.cjs +835 -0
- package/icon.svg +16 -0
- package/index.js +30 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +58 -4
- package/web/index.html +1887 -0
package/web/index.html
ADDED
|
@@ -0,0 +1,1887 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="zh-CN"><head>
|
|
3
|
+
<meta charset="utf-8">
|
|
4
|
+
<title>DSH Visualization · session instrument</title>
|
|
5
|
+
<style>
|
|
6
|
+
:root{
|
|
7
|
+
--ink:#eceff5; --mid:#a8b0bc; --dim:#767e8a;
|
|
8
|
+
--line:rgba(190,200,215,.20); --line2:rgba(190,200,215,.10);
|
|
9
|
+
--bg:#1b1e23; --bg2:#23272e;
|
|
10
|
+
--session:#e6edfa; --user:#8fe0c8; --tool:#ffc46b; --task:#f0a6c6;
|
|
11
|
+
--skill:#b28cff; --state:#8a919d; --ok:#8fe0c8; --bad:#ff8a6b;
|
|
12
|
+
}
|
|
13
|
+
*{box-sizing:border-box;margin:0}
|
|
14
|
+
html,body{height:100%;overflow:hidden;background:var(--bg);color:var(--ink);
|
|
15
|
+
font:11px/1.45 ui-monospace,"Cascadia Mono",Consolas,monospace;cursor:default}
|
|
16
|
+
#bg{position:fixed;inset:0;background:
|
|
17
|
+
radial-gradient(ellipse 64% 60% at 38% 44%, #31363e 0%, #282d34 40%, #1f2329 72%, #191c21 100%)}
|
|
18
|
+
#grain{position:fixed;inset:0;opacity:.2;pointer-events:none;
|
|
19
|
+
background-image:repeating-linear-gradient(0deg, rgba(255,255,255,.022) 0 1px, transparent 1px 3px)}
|
|
20
|
+
#net{position:fixed;inset:0;pointer-events:auto}
|
|
21
|
+
|
|
22
|
+
/* 悬停提示 + 点击后的详情卡 */
|
|
23
|
+
#tip{position:fixed;z-index:40;pointer-events:none;padding:3px 8px;background:rgba(10,12,16,.92);
|
|
24
|
+
border:1px solid rgba(190,200,215,.28);font-size:11px;color:var(--ink);letter-spacing:.04em;
|
|
25
|
+
opacity:0;transition:opacity .1s;white-space:nowrap}
|
|
26
|
+
#tip.on{opacity:1}
|
|
27
|
+
#tip em{font-style:normal;color:var(--dim);font-size:9px;margin-left:5px}
|
|
28
|
+
#tdetail{position:fixed;z-index:41;width:330px;padding:11px 13px;background:rgba(10,12,16,.96);
|
|
29
|
+
border:1px solid rgba(190,200,215,.3);font:10px/1.6 ui-monospace,Consolas,monospace;color:var(--mid);
|
|
30
|
+
display:none;box-shadow:0 8px 30px rgba(0,0,0,.6)}
|
|
31
|
+
#tdetail.on{display:block}
|
|
32
|
+
#tdetail h5{margin:0 0 7px;font-size:12px;color:#fff;letter-spacing:.06em;display:flex;align-items:baseline;gap:6px}
|
|
33
|
+
#tdetail h5 em{font-style:normal;font-size:9px;color:var(--dim)}
|
|
34
|
+
#tdetail .row{display:flex;justify-content:space-between;border-bottom:1px solid rgba(190,200,215,.10);padding:2px 0}
|
|
35
|
+
#tdetail .row b{color:var(--ink);font-weight:600;font-variant-numeric:tabular-nums}
|
|
36
|
+
#tdetail .sub{margin-top:8px;color:var(--dim);font-size:9px;letter-spacing:.14em}
|
|
37
|
+
#tdetail .cmd{display:flex;gap:7px;margin-top:3px;font-size:9px;line-height:1.45;color:var(--mid)}
|
|
38
|
+
#tdetail .cmd .t{flex:none;color:var(--dim);font-variant-numeric:tabular-nums}
|
|
39
|
+
#tdetail .cmd span:last-child{word-break:break-all;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
|
|
40
|
+
|
|
41
|
+
.dash{position:fixed;inset:0;pointer-events:none;display:grid;
|
|
42
|
+
grid-template-columns:1fr 430px;grid-template-rows:56px 1fr 226px;
|
|
43
|
+
grid-template-areas:"top top" "rings side" "log side"}
|
|
44
|
+
.lb{font-size:9px;letter-spacing:.24em;color:var(--dim);text-transform:uppercase}
|
|
45
|
+
|
|
46
|
+
/* 过滤片:点一下就只看这一类 */
|
|
47
|
+
.flt{display:flex;gap:5px;margin:-3px 0 8px;flex-wrap:wrap}
|
|
48
|
+
.flt b{font-size:9px;letter-spacing:.1em;color:#717885;border:1px solid rgba(190,200,215,.14);
|
|
49
|
+
padding:2px 8px;cursor:pointer;user-select:none;font-weight:500;transition:color .12s,border-color .12s}
|
|
50
|
+
.flt b:hover{color:#dfe4ec;border-color:rgba(190,200,215,.4)}
|
|
51
|
+
.flt b.on{color:#101216;background:#cfd6e0;border-color:#cfd6e0;font-weight:700}
|
|
52
|
+
|
|
53
|
+
/* 顶栏 */
|
|
54
|
+
.top{grid-area:top;pointer-events:auto;display:flex;align-items:center;gap:24px;padding:0 24px;border-bottom:1px solid var(--line2);position:relative}
|
|
55
|
+
/* 会话进度条:一轮一段,宽度 ∝ 该轮时长;当前轮亮琥珀 */
|
|
56
|
+
.tstrip{position:absolute;left:24px;right:24px;bottom:0;height:3px;display:flex;gap:2px;align-items:stretch}
|
|
57
|
+
.tstrip i{display:block;min-width:2px;background:rgba(190,200,215,.20);transition:background .2s}
|
|
58
|
+
.tstrip i.cur{background:var(--tool);box-shadow:0 0 8px rgba(255,196,107,.7)}
|
|
59
|
+
.tstrip i.err{background:rgba(255,138,107,.55)}
|
|
60
|
+
.brand b{font-size:13px;letter-spacing:.3em;color:#fff}
|
|
61
|
+
.brand span{font-size:9px;letter-spacing:.18em;color:var(--dim);margin-left:10px}
|
|
62
|
+
.ws{font-size:10px;color:var(--dim);max-width:34vw;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
63
|
+
.stats{display:flex;gap:20px;margin-left:auto;align-items:baseline}
|
|
64
|
+
.st i{display:block;font-size:9px;letter-spacing:.18em;color:var(--dim);font-style:normal}
|
|
65
|
+
.st b{font-size:16px;font-weight:800;color:#fff;font-variant-numeric:tabular-nums}
|
|
66
|
+
.st b.pink{color:var(--task)}
|
|
67
|
+
.stchip{border:1px solid currentColor;padding:3px 10px;font-size:10px;letter-spacing:.24em;color:var(--dim)}
|
|
68
|
+
.popout{cursor:pointer;font-size:10px;font-weight:400;letter-spacing:.08em;color:var(--dim);
|
|
69
|
+
border:1px solid rgba(190,200,215,.22);padding:2px 8px;border-radius:3px;user-select:none;white-space:nowrap;
|
|
70
|
+
text-decoration:none;display:inline-block}
|
|
71
|
+
.popout:hover{color:var(--ink);border-color:rgba(190,200,215,.5)}
|
|
72
|
+
.clk{font-size:11px;color:var(--dim);font-variant-numeric:tabular-nums;min-width:64px;text-align:right}
|
|
73
|
+
|
|
74
|
+
/* 环区 */
|
|
75
|
+
.rings{grid-area:rings;position:relative}
|
|
76
|
+
.core{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);text-align:center;pointer-events:none}
|
|
77
|
+
.core .cs{font-size:11px;letter-spacing:.42em;color:var(--mid)}
|
|
78
|
+
.core .cb{font-size:48px;line-height:1.02;font-weight:800;color:#fff;font-variant-numeric:tabular-nums;
|
|
79
|
+
text-shadow:0 0 40px rgba(170,205,255,.35)}
|
|
80
|
+
.core .cl{font-size:9px;letter-spacing:.4em;color:var(--dim)}
|
|
81
|
+
#link{position:absolute;left:24px;bottom:16px;font-size:10px;color:var(--dim);line-height:1.7;pointer-events:auto}
|
|
82
|
+
#link b{color:var(--mid);font-weight:600}
|
|
83
|
+
#link .bad{color:var(--bad)}
|
|
84
|
+
#pick{background:#1b1e23;color:var(--ink);border:1px solid rgba(190,200,215,.22);font:10px ui-monospace,Consolas,monospace;padding:2px 5px;max-width:360px;cursor:pointer}
|
|
85
|
+
#pick:focus{outline:1px solid rgba(200,215,240,.45)}
|
|
86
|
+
.chip{cursor:pointer;color:var(--mid);border:1px solid rgba(190,200,215,.22);padding:1px 8px;
|
|
87
|
+
display:inline-block;margin-right:6px;font-weight:500;user-select:none}
|
|
88
|
+
.chip:hover{color:#fff;border-color:rgba(200,215,240,.5)}
|
|
89
|
+
.chip.on{color:#101216;background:#cfd6e0;border-color:#cfd6e0;font-weight:700}
|
|
90
|
+
|
|
91
|
+
/* 右栏 */
|
|
92
|
+
.side{grid-area:side;pointer-events:auto;border-left:1px solid var(--line2);padding:18px 24px 14px;
|
|
93
|
+
display:flex;flex-direction:column;gap:18px;overflow:hidden}
|
|
94
|
+
.blk{min-height:0;display:flex;flex-direction:column}
|
|
95
|
+
.blk h4{display:flex;align-items:baseline;justify-content:space-between;margin-bottom:9px}
|
|
96
|
+
.blk h4 b{font-size:10px;color:var(--mid);font-weight:600;letter-spacing:.1em}
|
|
97
|
+
.ctxbar{height:6px;background:rgba(255,255,255,.08);position:relative;overflow:hidden}
|
|
98
|
+
.ctxbar i{position:absolute;left:0;top:0;bottom:0;background:linear-gradient(90deg,#6fd6bd,#eef1f7);width:0;transition:width .3s}
|
|
99
|
+
.ctxrow{display:flex;justify-content:space-between;align-items:baseline;margin-top:7px}
|
|
100
|
+
.ctxrow b{font-size:20px;font-weight:800;color:#fff;font-variant-numeric:tabular-nums}
|
|
101
|
+
.ctxrow span{font-size:10px;color:var(--dim)}
|
|
102
|
+
.kv{display:grid;grid-template-columns:1fr 1fr;gap:2px 16px;margin-top:10px}
|
|
103
|
+
.kv div{display:flex;justify-content:space-between;font-size:11px;padding:3px 0;border-bottom:1px solid var(--line2)}
|
|
104
|
+
.kv i{font-style:normal;color:var(--dim);font-size:9px;letter-spacing:.14em}
|
|
105
|
+
.kv b{font-weight:700;font-variant-numeric:tabular-nums}
|
|
106
|
+
|
|
107
|
+
.flow{position:relative}
|
|
108
|
+
.flow::before{content:"";position:absolute;left:2.5px;top:8px;bottom:14px;width:1px;
|
|
109
|
+
background:linear-gradient(180deg,var(--line),var(--line2))}
|
|
110
|
+
.fnode{display:grid;grid-template-columns:8px 62px 1fr 96px;align-items:baseline;gap:9px;
|
|
111
|
+
padding:7px 0;border-bottom:1px solid var(--line2);position:relative}
|
|
112
|
+
.fnode .fd{width:6px;height:6px;background:currentColor;box-shadow:0 0 9px currentColor}
|
|
113
|
+
.fnode i{font-style:normal;font-size:9px;letter-spacing:.2em;color:var(--mid)}
|
|
114
|
+
.fnode b{font-size:17px;font-weight:800;color:#fff;text-align:right;font-variant-numeric:tabular-nums}
|
|
115
|
+
.fnode em{font-style:normal;font-size:9px;color:var(--dim);text-align:right;letter-spacing:.08em}
|
|
116
|
+
|
|
117
|
+
.tb{font-size:11px;overflow-y:auto;overflow-x:hidden;min-height:0;flex:1;padding-right:4px;scrollbar-width:thin;scrollbar-color:rgba(190,200,215,.28) transparent}
|
|
118
|
+
.tb::-webkit-scrollbar{width:6px}
|
|
119
|
+
.tb::-webkit-scrollbar-thumb{background:rgba(190,200,215,.28)}
|
|
120
|
+
.tb::-webkit-scrollbar-track{background:transparent}
|
|
121
|
+
.tb .rw{display:grid;gap:9px;align-items:center;padding:4px 0;border-bottom:1px solid var(--line2)}
|
|
122
|
+
.tb .hdrow{color:var(--dim);font-size:9px;letter-spacing:.16em;border-bottom:1px solid var(--line)}
|
|
123
|
+
.tb .rw span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
124
|
+
#taskTable .rw{grid-template-columns:64px 1fr 52px 40px}
|
|
125
|
+
#skillTable .rw{grid-template-columns:1fr 36px 56px}
|
|
126
|
+
#callTable .rw{grid-template-columns:1fr 40px 58px 44px}
|
|
127
|
+
|
|
128
|
+
/* 轮次时间线:一轮两行 —— 上面编号+统计,下面你当时说了什么 */
|
|
129
|
+
#turnTable .row2{display:block;padding:5px 0 6px;border-bottom:1px solid var(--line2)}
|
|
130
|
+
#turnTable .l1{display:flex;justify-content:space-between;align-items:baseline;gap:8px}
|
|
131
|
+
#turnTable .no{font-size:10px;color:var(--dim);font-variant-numeric:tabular-nums;letter-spacing:.08em}
|
|
132
|
+
#turnTable .meta{font-size:9px;color:var(--dim);font-variant-numeric:tabular-nums}
|
|
133
|
+
#turnTable .ask{font-size:11px;color:var(--mid);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-top:2px}
|
|
134
|
+
#turnTable .row2.cur .no{color:var(--tool);font-weight:700}
|
|
135
|
+
#turnTable .row2.cur .ask{color:var(--ink);font-weight:600}
|
|
136
|
+
#turnTable .row2.cur{background:linear-gradient(90deg,rgba(255,196,107,.07),transparent)}
|
|
137
|
+
|
|
138
|
+
/* 事件表里的轮次分隔条 */
|
|
139
|
+
.tsep{display:flex;justify-content:space-between;align-items:baseline;gap:10px;
|
|
140
|
+
padding:7px 0 4px;margin-top:2px;border-bottom:1px solid var(--line);
|
|
141
|
+
font-size:9px;letter-spacing:.14em;color:var(--dim)}
|
|
142
|
+
.tsep span:first-child{color:var(--mid);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
143
|
+
.tsep span:last-child{flex:none;font-variant-numeric:tabular-nums}
|
|
144
|
+
.run{color:var(--ok)}.done{color:var(--dim)}.bad{color:var(--bad)}
|
|
145
|
+
|
|
146
|
+
/* 底部事件表 */
|
|
147
|
+
.log{grid-area:log;pointer-events:auto;border-top:1px solid var(--line);padding:12px 24px 0;display:flex;flex-direction:column;min-height:0;overflow:hidden}
|
|
148
|
+
.log .tb{flex:1;min-height:0;padding-right:6px}
|
|
149
|
+
.log .hd{display:flex;align-items:baseline;justify-content:space-between;margin-bottom:8px}
|
|
150
|
+
#rows .rw{grid-template-columns:60px 66px 132px 1fr 48px 34px}
|
|
151
|
+
.ly.session{color:var(--session)}.ly.user{color:var(--user)}.ly.tool{color:var(--tool)}
|
|
152
|
+
.ly.task{color:var(--task)}.ly.skill{color:var(--skill)}.ly.state{color:var(--state)}
|
|
153
|
+
#rows .rw .nm{color:var(--ink);font-weight:700}
|
|
154
|
+
#rows .rw span{color:var(--mid)}
|
|
155
|
+
.credit{position:absolute;right:24px;bottom:6px;font-size:9px;color:#5a616b}
|
|
156
|
+
@media (max-width:1520px){.dash{grid-template-columns:1fr 380px}.side{padding-left:18px;padding-right:18px}}
|
|
157
|
+
</style>
|
|
158
|
+
</head>
|
|
159
|
+
<body>
|
|
160
|
+
<div id="bg"></div>
|
|
161
|
+
<div id="grain"></div>
|
|
162
|
+
<canvas id="net" width="1997" height="1282" style="width: 1997px; height: 1282px; cursor: default;"></canvas>
|
|
163
|
+
<div id="tip" class="" style="left: 1226px; top: 305px;"></div>
|
|
164
|
+
<div id="tdetail"></div>
|
|
165
|
+
|
|
166
|
+
<div class="dash">
|
|
167
|
+
<header class="top">
|
|
168
|
+
<div class="brand"><b>DSH Visualization</b><span>SESSION INSTRUMENT</span></div>
|
|
169
|
+
<div class="ws" id="tTitle">dsh工作可视化</div>
|
|
170
|
+
<div class="stats">
|
|
171
|
+
<div class="st"><i id="tTurnLb">轮次 / 58</i><b id="tTurn">58</b></div>
|
|
172
|
+
<div class="st"><i>步骤</i><b id="tStep">60</b></div>
|
|
173
|
+
<div class="st"><i>工具调用</i><b id="tTools">803</b></div>
|
|
174
|
+
<div class="st"><i>本步耗时</i><b class="pink" id="tTasks">4.8s</b></div>
|
|
175
|
+
<div class="st"><i>技能</i><b id="tSkills">1</b></div>
|
|
176
|
+
<div class="stchip" id="tState" style="color: rgb(138, 145, 157);">IDLE</div>
|
|
177
|
+
</div>
|
|
178
|
+
<div class="clk" id="tClock">20:19:29</div>
|
|
179
|
+
<a class="popout" id="tPop" href="#" target="_blank" rel="noopener"
|
|
180
|
+
title="在新窗口打开,可拖到另一个屏幕">⧉ 新窗口</a>
|
|
181
|
+
<div class="tstrip" id="tstrip"></div>
|
|
182
|
+
</header>
|
|
183
|
+
|
|
184
|
+
<div class="rings">
|
|
185
|
+
<div class="core">
|
|
186
|
+
<div class="cs" id="cState" style="color: rgb(138, 145, 157);">IDLE</div>
|
|
187
|
+
<div class="cb" id="cTurn">58</div>
|
|
188
|
+
<div class="cl">TURN</div>
|
|
189
|
+
</div>
|
|
190
|
+
<div id="link">
|
|
191
|
+
<div><b id="lkSrc">数据源:session-6a016319-917… · 3.86MB</b></div>
|
|
192
|
+
<div id="lkMeta">实时 · 0.0s 前更新 · 5293 条事件 · preset standard</div>
|
|
193
|
+
<div style="margin-top:6px"><b>会话:</b><select id="pick" data-auto="1"><option value="">自动 · 跟最新会话</option><option value="session-6a016319-9178-4fdc-b76b-e0e622ca8b17">6a016319 · dsh-Visualization · 11/7 20:07</option><option value="session-a8e93239-904a-4f4b-b381-1602b38b014b">a8e93239 · default-workspace · 11/7 15:42</option><option value="session-12e9a6c6-f16a-4ab8-ba43-c71cbefbcc2c">12e9a6c6 · default-workspace · 11/7 14:05</option><option value="session-dbbb1e1b-44ce-4288-96de-13c465684d9c">dbbb1e1b · default-workspace · 11/7 11:13</option><option value="session-2bac112f-5180-4962-a181-c018bc51c5f5">2bac112f · default-workspace · 11/6 19:46</option><option value="session-ca6d8d75-1214-4662-9a27-d6eed9fc62ae">ca6d8d75 · default-workspace · 11/6 12:39</option><option value="session-da6bbaa3-c64f-4d5a-ab3a-5880056138f0">da6bbaa3 · menote · 11/2 22:41</option><option value="session-f4c6837c-d0ec-4eb4-844e-24bc85e885e0">f4c6837c · effects · 11/1 11:41</option><option value="session-fa0d89b8-9842-47c2-8580-288d83960b7b">fa0d89b8 · default-workspace · 10/31 12:08</option><option value="session-1ef3a3ae-5a23-447e-ad2f-c299fce44b2e">1ef3a3ae · godot-dsh-学习项目 · 10/30 12:57</option><option value="session-387a4436-e8a0-413c-8fc4-5f78605b8fa0">387a4436 · default-workspace · 10/29 20:30</option><option value="session-0427c851-3ba0-4c1a-9cdb-f956ff2cec3d">0427c851 · 东方穹城录 · 10/29 19:17</option><option value="session-e2351f7a-c50f-4b75-97c1-02c36d5c331d">e2351f7a · default-workspace · 10/29 12:39</option><option value="session-ec7cd40f-68ba-4d51-94fd-2ee444313f02">ec7cd40f · default-workspace · 10/29 12:14</option><option value="session-22e5cafc-3604-4b29-93b5-897332de73eb">22e5cafc · default-workspace · 10/28 20:36</option><option value="session-65958b80-5aa4-46f9-8016-8a0baf6ba32a">65958b80 · default-workspace · 10/28 17:09</option><option value="session-c1f5eb50-0ead-4b9f-9f39-36291864ecc8">c1f5eb50 · default-workspace · 10/28 11:49</option><option value="session-49ec6397-e4ba-41fe-99af-4121c909d1c7">49ec6397 · default-workspace · 10/28 10:36</option><option value="session-1141f1ef-f7f6-4663-b388-0294c12560a3">1141f1ef · dsh-workspace · 10/27 12:57</option><option value="session-ca9b49e1-5306-4fc2-bf87-b1c3a0b5033a">ca9b49e1 · life · 10/25 20:15</option><option value="session-a2234ef9-416f-406b-9c80-39f43a08f20c">a2234ef9 · 我的脑库 · 10/25 16:53</option></select></div>
|
|
194
|
+
<div style="margin-top:7px" id="ringToggles"></div>
|
|
195
|
+
<div style="margin-top:7px"><b id="rotBtn" style="cursor:pointer;color:var(--mid);border:1px solid rgba(190,200,215,.22);padding:1px 8px;display:inline-block">环旋转:开</b><b id="cnBtn" class="chip" style="margin-left:6px">连线:两者</b><b id="shBtn" class="chip">占比:次数</b><b id="tnBtn" class="chip">连线轮数:1</b><b id="scBtn" class="chip">显示轮数:1</b><b id="langBtn" class="chip">中 / EN</b></div>
|
|
196
|
+
<div style="margin-top:7px">
|
|
197
|
+
<b id="rpPlay" class="chip">▶ 重播</b><b id="rpSpeed" class="chip">90ms/条</b><b id="rpLive" class="chip" style="display:none">回到实时</b>
|
|
198
|
+
<span id="rpProg" style="color:var(--dim);font-size:9px;margin-left:4px"></span>
|
|
199
|
+
</div>
|
|
200
|
+
</div>
|
|
201
|
+
</div>
|
|
202
|
+
|
|
203
|
+
<aside class="side">
|
|
204
|
+
<section class="blk">
|
|
205
|
+
<h4><span class="lb">会话逻辑 / 谁驱动了谁</span><b>↓ 因果</b></h4>
|
|
206
|
+
<div class="flow">
|
|
207
|
+
<div class="fnode" style="color:var(--session)"><span class="fd"></span><i>会话</i><b id="fS">58</b><em id="fS2">step 60</em></div>
|
|
208
|
+
<div class="fnode" style="color:var(--task)"><span class="fd"></span><i>步骤</i><b id="fA">60</b><em id="fA2">本步 4.8s</em></div>
|
|
209
|
+
<div class="fnode" style="color:var(--skill)"><span class="fd"></span><i>技能</i><b id="fK">1</b><em id="fK2">已加载</em></div>
|
|
210
|
+
<div class="fnode" style="color:var(--tool)"><span class="fd"></span><i>工具</i><b id="fT">803</b><em id="fT2">calls</em></div>
|
|
211
|
+
<div class="fnode" style="color:var(--user)"><span class="fd"></span><i>用量</i><b id="fO">537.7k</b><em id="fO2">in 1.24M</em></div>
|
|
212
|
+
</div>
|
|
213
|
+
</section>
|
|
214
|
+
|
|
215
|
+
<section class="blk">
|
|
216
|
+
<h4><span class="lb">上下文占用 / CONTEXT</span><b id="sTps">3 tok/s</b></h4>
|
|
217
|
+
<div class="ctxbar"><i id="ctxBar" style="width: 21%;"></i></div>
|
|
218
|
+
<div class="ctxrow"><b id="ctxPct">21%</b><span id="ctxTok">219.6k / 1.05M</span></div>
|
|
219
|
+
<div class="kv">
|
|
220
|
+
<div><i>输入</i><b id="tokIn">1.24M</b></div>
|
|
221
|
+
<div><i>输出</i><b id="tokOut">537.7k</b></div>
|
|
222
|
+
<div><i>缓存</i><b id="tokCache">366.42M</b></div>
|
|
223
|
+
<div><i>模型</i><b id="tokModel" title="deepseek-flash">deepseek-flash</b></div>
|
|
224
|
+
</div>
|
|
225
|
+
</section>
|
|
226
|
+
|
|
227
|
+
<section class="blk">
|
|
228
|
+
<h4><span class="lb">后台任务 / 工具在后台继续跑</span><b id="sJobs">1 运行中 / 共 38</b></h4>
|
|
229
|
+
<div class="flt" id="fJobs"><b data-f="jobs:all" class="on">全部</b><b data-f="jobs:run" class="">运行中</b><b data-f="jobs:done" class="">已结束</b></div>
|
|
230
|
+
<div class="tb" id="taskTable" style="max-height:108px"></div>
|
|
231
|
+
</section>
|
|
232
|
+
|
|
233
|
+
<section class="blk" style="flex:1.7">
|
|
234
|
+
<h4><span class="lb">轮次时间线 / 这个会话怎么走过来的</span><b id="sTurns">58 轮</b></h4>
|
|
235
|
+
<div class="tb" id="turnTable"></div>
|
|
236
|
+
</section>
|
|
237
|
+
|
|
238
|
+
<section class="blk" style="flex:0.9">
|
|
239
|
+
<h4><span class="lb">工具统计 / 每种工具累计干了多少</span><b id="sCall">59 次 / 6 种</b></h4>
|
|
240
|
+
<div class="flt" id="fTools"><b data-f="tools:all" class="on">全部</b><b data-f="tools:fail" class="">有失败</b><b data-f="tools:slow" class="">耗时 >1s</b></div>
|
|
241
|
+
<div class="tb" id="callTable"></div>
|
|
242
|
+
</section>
|
|
243
|
+
</aside>
|
|
244
|
+
|
|
245
|
+
<section class="log">
|
|
246
|
+
<div class="hd">
|
|
247
|
+
<span class="lb">Event log / 真实事件</span>
|
|
248
|
+
<span class="lb" id="logHint">共 149 条 · 按轮分组 · 新在最上</span>
|
|
249
|
+
</div>
|
|
250
|
+
<div class="flt" id="fLog"><b data-f="log:all" class="on">全部</b><b data-f="log:tool" class="">工具</b><b data-f="log:task" class="">任务</b><b data-f="log:session" class="">会话</b><b data-f="log:skill" class="">技能</b><b data-f="log:user" class="">用户</b></div>
|
|
251
|
+
<div class="tb" id="rows"></div>
|
|
252
|
+
</section>
|
|
253
|
+
<div class="credit">DSH Visualization by Ryder191 · PolyForm NC · visual language: HALOFLUX</div>
|
|
254
|
+
</div>
|
|
255
|
+
|
|
256
|
+
<script>
|
|
257
|
+
(function () {
|
|
258
|
+
'use strict';
|
|
259
|
+
var TAU = Math.PI * 2;
|
|
260
|
+
var $ = function (id) { return document.getElementById(id); };
|
|
261
|
+
|
|
262
|
+
/* ---------------- 状态:全部来自 /state ---------------- */
|
|
263
|
+
var M = {
|
|
264
|
+
ok: false, state: 'OFFLINE', turn: 0, step: 0, turns: 0, turnOpen: false,
|
|
265
|
+
ctxPct: 0, ctxUsed: 0, ctxWin: 0, tps: 0,
|
|
266
|
+
tok: { in: 0, out: 0, cache: 0 },
|
|
267
|
+
tools: [], toolCount: 0, jobs: [], skills: [], log: [],
|
|
268
|
+
subagents: 0, approvals: 0, todos: 0, errs: 0,
|
|
269
|
+
session: {}, bridge: {}, model: '—', lastUpdate: 0, stale: false,
|
|
270
|
+
/* 按"显示轮数"收窄后的数组,render 里赋值;先给空数组,免得 draw 在首帧读到 undefined */
|
|
271
|
+
sSteps: [], sJobs: [], sTools: [], sLog: [], sStats: []
|
|
272
|
+
};
|
|
273
|
+
var COL = { session: '#e6edfa', task: '#f0a6c6', skill: '#b28cff', tool: '#ffc46b' };
|
|
274
|
+
|
|
275
|
+
/* 七层环:显隐多选、身份色、旋转速度 */
|
|
276
|
+
var RINGVIS = [true, true, true, true, false, false, false];
|
|
277
|
+
var RING_NAME = ['① 会话', '② 步骤', '③ 技能', '④ 工具', '⑤ 任务', '⑥ 审批', '⑦ 变更'];
|
|
278
|
+
var RING_COL = ['#e6edfa', '#f0a6c6', '#b28cff', '#ffc46b', '#7fd8c4', '#ff8a6b', '#8fb8ff'];
|
|
279
|
+
|
|
280
|
+
/* 连线画在环「下面」:本帧只算几何,下一帧在画环之前先画出来(1 帧延迟,肉眼无感) */
|
|
281
|
+
var CHAIN_PREV = [];
|
|
282
|
+
/* ② 环本帧的分格几何(连线要把落点对到格子中心,不能对到精确时刻) */
|
|
283
|
+
var R2GEO = null;
|
|
284
|
+
/* 各层自己的时间尺度(⑥⑦)和弧段位置(⑤):连线必须用它们自己的坐标系 */
|
|
285
|
+
var RSPAN = {};
|
|
286
|
+
var JOBARC = {};
|
|
287
|
+
var SKILL_ANG = null;
|
|
288
|
+
function segLine(p0, p1, bright, fr) {
|
|
289
|
+
X2.beginPath(); X2.moveTo(p0.x, p0.y); X2.lineTo(p1.x, p1.y);
|
|
290
|
+
X2.strokeStyle = bright
|
|
291
|
+
? 'rgba(255,236,205,' + (0.16 + 0.5 * fr).toFixed(3) + ')'
|
|
292
|
+
: 'rgba(168,184,210,0.30)';
|
|
293
|
+
X2.lineWidth = bright ? (1 + 1.4 * fr) : 1.1;
|
|
294
|
+
X2.stroke();
|
|
295
|
+
X2.beginPath(); X2.arc(p1.x, p1.y, bright ? 1.8 : 1.1, 0, TAU);
|
|
296
|
+
X2.fillStyle = bright ? 'rgba(255,246,230,.8)' : 'rgba(168,184,210,.45)';
|
|
297
|
+
X2.fill();
|
|
298
|
+
}
|
|
299
|
+
/* 模块级的画布上下文(draw 里的 X 是局部变量,模块级函数用不到) */
|
|
300
|
+
var X2 = null;
|
|
301
|
+
|
|
302
|
+
/* 命中测试区域(每帧由 draw 填充):④ 环上每个工具扇区的起止角 */
|
|
303
|
+
/* 命中表:每层环画出节点时把 { kind, r, a0, a1, label, data } 登记进来,鼠标统一在这里查 */
|
|
304
|
+
var HIT = { ox: 0, oy: 0, items: [], ok: false };
|
|
305
|
+
function hitReg(o) { HIT.items.push(o); }
|
|
306
|
+
|
|
307
|
+
/* 三环旋转:② 步骤慢、③ 技能更慢、④ 工具最快;① 会话永远不转(它是坐标系)
|
|
308
|
+
rad/s。旋转后「12 点 = 现在」不再成立,最新节点会自己漂走 —— 点右下角可以关掉。 */
|
|
309
|
+
var ROT = { on: true, sp: [0, 0.030, 0.018, 0.090, 0.012, 0.006, 0.020] };
|
|
310
|
+
function rotOf(i, t) { return ROT.on ? ROT.sp[i] * t : 0; }
|
|
311
|
+
|
|
312
|
+
/* 连线方式:node = 节点之间连(顺序链);beam = 从核心射向在跑的节点;both = 两种都要 */
|
|
313
|
+
var CN = { mode: 'both', list: ['node', 'beam', 'both'], label: { node: '节点连线', beam: '中心光束', both: '两者都有' }, share: 'count', turns: 2, turnOpts: [1, 2, 3, 5, 8, 0] };
|
|
314
|
+
/* 显示范围:按轮次收窄(0 = 全部)。影响四环、右栏表、事件表,统一口径 */
|
|
315
|
+
var SCOPE = { turns: 0, opts: [1, 2, 3, 5, 8, 0] };
|
|
316
|
+
function scopeMinTurn() {
|
|
317
|
+
var tl = M.turnsList || [];
|
|
318
|
+
if (!SCOPE.turns || tl.length <= SCOPE.turns) return -Infinity;
|
|
319
|
+
return tl[tl.length - SCOPE.turns].no;
|
|
320
|
+
}
|
|
321
|
+
function inScope(x) { return (x.turn || 0) >= scopeMinTurn(); }
|
|
322
|
+
function aggTools(list) {
|
|
323
|
+
var map = {};
|
|
324
|
+
list.forEach(function (x) {
|
|
325
|
+
var s = map[x.name] || (map[x.name] = { name: x.name, count: 0, ms: 0, fail: 0, last: 0 });
|
|
326
|
+
s.count++;
|
|
327
|
+
if (x.ms != null) s.ms += x.ms;
|
|
328
|
+
if (x.ok === false) s.fail++;
|
|
329
|
+
if (x.t > s.last) s.last = x.t;
|
|
330
|
+
});
|
|
331
|
+
return Object.keys(map).map(function (k) { return map[k] }).sort(function (a, b2) { return b2.count - a.count });
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/* 英文名 → 中文说法。查不到就原样显示,不编 */
|
|
335
|
+
var TOOL_CN = {
|
|
336
|
+
read: '读取文件', read_file: '读取文件', write: '写入文件', edit: '修改文件',
|
|
337
|
+
glob: '查找文件', grep: '搜索内容', ls: '列目录',
|
|
338
|
+
bash: '执行命令', pwsh: '执行命令', terminal: '打开终端',
|
|
339
|
+
web_fetch: '抓取网页', web_search: '联网搜索',
|
|
340
|
+
read_image: '查看图片', present: '交付文件',
|
|
341
|
+
todo_write: '更新待办', update_goal: '更新目标', get_goal: '读取目标', create_goal: '设立目标',
|
|
342
|
+
skill: '加载技能', subagent: '派发子任务', subagent_fork: '派发子任务',
|
|
343
|
+
job_output: '读后台输出', job_kill: '停止后台任务', job_list: '列后台任务',
|
|
344
|
+
ask_user_question: '询问用户', schedule_create: '建定时任务', schedule_edit: '改定时任务',
|
|
345
|
+
schedule_list: '列定时任务', schedule_view: '查看定时任务',
|
|
346
|
+
plugin_manager: '管理插件', cordis_inspect_list: '查运行时清单', cordis_inspect_query: '查运行时契约',
|
|
347
|
+
session_delete: '删除会话', workflow: '跑工作流', task: '任务'
|
|
348
|
+
};
|
|
349
|
+
var SKILL_CN = {
|
|
350
|
+
'dsh-plugin-dev': '插件开发规程', 'archify': '架构图绘制',
|
|
351
|
+
'diagnose-windows-sandbox-acl': '沙箱权限诊断', 'agent-experience': '工具与技能设计',
|
|
352
|
+
'cordis-plugin-development': 'Cordis 插件开发', 'editing-cordis-compositions': '编辑装配清单',
|
|
353
|
+
'cordis-composition-reference': '装配清单参考', 'office': '办公文档', 'office-docx': 'Word 文档',
|
|
354
|
+
'office-xlsx': 'Excel 表格', 'office-pptx': 'PPT 演示', 'office-export-pdf': '导出 PDF',
|
|
355
|
+
'office-recalculate': '重算表格', 'office-render': '渲染文档'
|
|
356
|
+
};
|
|
357
|
+
var JOB_CN = { pwsh: '后台命令', bash: '后台命令', node: '后台进程', terminal: '终端', job: '后台任务' };
|
|
358
|
+
|
|
359
|
+
function cn(map, key) { return (map && map[key]) || '' }
|
|
360
|
+
/* 一行里同时给中文和原名:中文在前,原名做暗色备注 */
|
|
361
|
+
function named(map, key) {
|
|
362
|
+
var zh = cn(map, key);
|
|
363
|
+
if (!zh) return key;
|
|
364
|
+
return zh + ' <span style="color:var(--dim);font-size:9px">' + key + '</span>';
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/* ---------------- 过滤 ---------------- */
|
|
368
|
+
var FILTER = { jobs: 'all', tools: 'all', log: 'all' };
|
|
369
|
+
var FL = {
|
|
370
|
+
jobs: [['all', '全部'], ['run', '运行中'], ['done', '已结束']],
|
|
371
|
+
tools: [['all', '全部'], ['fail', '有失败'], ['slow', '耗时 >1s']],
|
|
372
|
+
log: [['all', '全部'], ['tool', '工具'], ['task', '任务'], ['session', '会话'], ['skill', '技能'], ['user', '用户']]
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
/* 跟随宿主:/dsh-visualization/?session=<id> 时,state 只取那个会话 */
|
|
376
|
+
function API_URL(base) {
|
|
377
|
+
var s = '';
|
|
378
|
+
try { s = new URLSearchParams(location.search).get('session') || '' } catch (e) { }
|
|
379
|
+
return s ? (base + '?session=' + encodeURIComponent(s)) : base;
|
|
380
|
+
}
|
|
381
|
+
function STATE_URL() { return API_URL('state'); }
|
|
382
|
+
|
|
383
|
+
/* ---------------- 显示选项持久化 ---------------- */
|
|
384
|
+
var PREFS_KEY = 'dshviz.prefs.v1';
|
|
385
|
+
/* 从旧名字一次性迁移:haloflux.* → dshviz.*(迁移完保留旧键,不做破坏性删除) */
|
|
386
|
+
(function () {
|
|
387
|
+
var pairs = [
|
|
388
|
+
['haloflux.prefs.v1', PREFS_KEY],
|
|
389
|
+
['haloflux.lang', 'dshviz.lang'],
|
|
390
|
+
['haloflux.debug', 'dshviz.debug'],
|
|
391
|
+
['haloflux.pop.size', 'dshviz.pop.size'],
|
|
392
|
+
];
|
|
393
|
+
try {
|
|
394
|
+
for (var i = 0; i < pairs.length; i++) {
|
|
395
|
+
var oldV = localStorage.getItem(pairs[i][0]);
|
|
396
|
+
if (oldV != null && localStorage.getItem(pairs[i][1]) == null) localStorage.setItem(pairs[i][1], oldV);
|
|
397
|
+
}
|
|
398
|
+
} catch (e) { }
|
|
399
|
+
})();
|
|
400
|
+
function syncAllBtns() {
|
|
401
|
+
var cn = $('cnBtn'), sh = $('shBtn'), rb = $('rotBtn');
|
|
402
|
+
if (cn) cn.textContent = '连线:' + CN.label[CN.mode];
|
|
403
|
+
if (sh) sh.textContent = '占比:' + (CN.share === 'count' ? '次数' : '耗时');
|
|
404
|
+
if (rb) {
|
|
405
|
+
rb.textContent = '环旋转:' + (ROT.on ? '开' : '关');
|
|
406
|
+
rb.style.color = ROT.on ? 'var(--mid)' : 'var(--dim)';
|
|
407
|
+
}
|
|
408
|
+
syncTurnBtns();
|
|
409
|
+
}
|
|
410
|
+
function savePrefs() {
|
|
411
|
+
try {
|
|
412
|
+
localStorage.setItem(PREFS_KEY, JSON.stringify({
|
|
413
|
+
rings: RINGVIS.slice(),
|
|
414
|
+
scope: SCOPE.turns,
|
|
415
|
+
lines: CN.turns,
|
|
416
|
+
mode: CN.mode,
|
|
417
|
+
share: CN.share,
|
|
418
|
+
rot: ROT.on,
|
|
419
|
+
filter: FILTER.log, fj: FILTER.jobs, ft: FILTER.tools
|
|
420
|
+
}));
|
|
421
|
+
} catch (e) { }
|
|
422
|
+
}
|
|
423
|
+
function applyPrefs() {
|
|
424
|
+
var s = null;
|
|
425
|
+
try { s = JSON.parse(localStorage.getItem(PREFS_KEY) || 'null') } catch (e) { }
|
|
426
|
+
if (!s) return;
|
|
427
|
+
if (Array.isArray(s.rings) && s.rings.length === RINGVIS.length) {
|
|
428
|
+
for (var i = 0; i < RINGVIS.length; i++) RINGVIS[i] = !!s.rings[i];
|
|
429
|
+
}
|
|
430
|
+
if (typeof s.scope === 'number' && SCOPE.opts.indexOf(s.scope) >= 0) SCOPE.turns = s.scope;
|
|
431
|
+
if (typeof s.lines === 'number' && CN.turnOpts.indexOf(s.lines) >= 0) CN.turns = s.lines;
|
|
432
|
+
if (s.mode && CN.list.indexOf(s.mode) >= 0) CN.mode = s.mode;
|
|
433
|
+
if (s.share === 'count' || s.share === 'ms') CN.share = s.share;
|
|
434
|
+
if (typeof s.rot === 'boolean') ROT.on = s.rot;
|
|
435
|
+
if (s.filter) FILTER.log = s.filter;
|
|
436
|
+
if (s.fj) FILTER.jobs = s.fj;
|
|
437
|
+
if (s.ft) FILTER.tools = s.ft;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function fltBox(k) { return $('f' + k.charAt(0).toUpperCase() + k.slice(1)); }
|
|
441
|
+
function buildFilters() {
|
|
442
|
+
Object.keys(FL).forEach(function (k) {
|
|
443
|
+
var box = fltBox(k);
|
|
444
|
+
if (!box) return;
|
|
445
|
+
box.innerHTML = FL[k].map(function (o) {
|
|
446
|
+
return '<b data-f="' + k + ':' + o[0] + '">' + o[1] + '</b>';
|
|
447
|
+
}).join('');
|
|
448
|
+
});
|
|
449
|
+
document.addEventListener('click', function (e) {
|
|
450
|
+
var el = e.target;
|
|
451
|
+
var key = el && el.getAttribute && el.getAttribute('data-f');
|
|
452
|
+
if (!key) return;
|
|
453
|
+
var p = key.split(':');
|
|
454
|
+
FILTER[p[0]] = p[1];
|
|
455
|
+
syncFilters();
|
|
456
|
+
savePrefs();
|
|
457
|
+
render();
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
function syncFilters() {
|
|
461
|
+
Object.keys(FL).forEach(function (k) {
|
|
462
|
+
var box = fltBox(k);
|
|
463
|
+
if (!box) return;
|
|
464
|
+
var bs = box.querySelectorAll('b');
|
|
465
|
+
for (var i = 0; i < bs.length; i++) {
|
|
466
|
+
var v = bs[i].getAttribute('data-f').split(':')[1];
|
|
467
|
+
bs[i].className = (v === FILTER[k]) ? 'on' : '';
|
|
468
|
+
}
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function fmt(n) {
|
|
473
|
+
n = Math.round(n || 0);
|
|
474
|
+
if (n >= 1e6) return (n / 1e6).toFixed(2) + 'M';
|
|
475
|
+
return n >= 1000 ? (n / 1000).toFixed(1) + 'k' : String(n);
|
|
476
|
+
}
|
|
477
|
+
function hhmmss(ms) {
|
|
478
|
+
var d = ms ? new Date(ms) : new Date(), p = function (x) { return (x < 10 ? '0' : '') + x; };
|
|
479
|
+
return p(d.getHours()) + ':' + p(d.getMinutes()) + ':' + p(d.getSeconds());
|
|
480
|
+
}
|
|
481
|
+
/* 重放:桥给出虚拟时间 vtime,页面在两轮拉取之间按速度自己推进,保证 60fps 顺滑 */
|
|
482
|
+
var RP = { on: false, mode: 'seq', playing: false, speed: 50, vtime: 0, t0: 0, t1: 0, anchor: 0, pct: 0, done: false, idx: 0, total: 0 };
|
|
483
|
+
function clockNow() {
|
|
484
|
+
if (!RP.on) return Date.now();
|
|
485
|
+
if (RP.mode === 'seq') return Math.min(RP.t1, RP.vtime); /* 事件序列:时间由事件决定,不自己推进 */
|
|
486
|
+
if (!RP.playing) return Math.min(RP.t1, RP.vtime);
|
|
487
|
+
return Math.min(RP.t1, RP.vtime + (Date.now() - RP.anchor) * RP.speed);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function ago(ms) {
|
|
491
|
+
if (!ms) return '—';
|
|
492
|
+
var s = (clockNow() - ms) / 1000;
|
|
493
|
+
if (s < 60) return s.toFixed(0) + 's';
|
|
494
|
+
if (s < 3600) return (s / 60).toFixed(0) + 'm';
|
|
495
|
+
return (s / 3600).toFixed(1) + 'h';
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/* ---------------- 会话选择 ---------------- */
|
|
499
|
+
function loadSessions() {
|
|
500
|
+
fetch('sessions', { cache: 'no-store' }).then(function (r) { return r.json() }).then(function (j) {
|
|
501
|
+
var sel = $('pick');
|
|
502
|
+
if (!sel) return;
|
|
503
|
+
var pad = function (x) { return (x < 10 ? '0' : '') + x; };
|
|
504
|
+
var html = '<option value="">自动 · 跟最新会话</option>';
|
|
505
|
+
(j.sessions || []).forEach(function (s) {
|
|
506
|
+
var base = (s.cwd || s.ws || '').replace(/^.*[\\\/]/, '');
|
|
507
|
+
var d = new Date(s.mtime);
|
|
508
|
+
html += '<option value="' + s.id + '">' + s.short + ' · ' + base + ' · ' +
|
|
509
|
+
(d.getMonth() + 1) + '/' + d.getDate() + ' ' + pad(d.getHours()) + ':' + pad(d.getMinutes()) + '</option>';
|
|
510
|
+
});
|
|
511
|
+
sel.innerHTML = html;
|
|
512
|
+
sel.value = j.auto ? '' : ((j.current && j.current.id) || '');
|
|
513
|
+
sel.dataset.auto = j.auto ? '1' : '';
|
|
514
|
+
}).catch(function () { });
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/* ---------------- 拉数据 ---------------- */
|
|
518
|
+
var fails = 0;
|
|
519
|
+
function applyState(j) {
|
|
520
|
+
fails = 0;
|
|
521
|
+
if (!j || !j.ok) { M.ok = false; M.state = 'NO DATA'; M.err = (j && j.error) || ''; render(); return }
|
|
522
|
+
M.ok = true; M.err = '';
|
|
523
|
+
M.state = j.state; M.turn = j.turn; M.step = j.step; M.turns = j.turns;
|
|
524
|
+
M.turnOpen = j.turnOpen; M.ctxPct = j.ctxPct; M.ctxUsed = j.ctxUsed; M.ctxWin = j.ctxWin;
|
|
525
|
+
M.tps = j.tps; M.tok = j.tok || M.tok; M.tools = j.tools || []; M.toolCount = j.toolCount || 0;
|
|
526
|
+
M.jobs = j.jobs || []; M.skills = j.skills || []; M.log = j.log || [];
|
|
527
|
+
M.steps = j.steps || []; M.turnOpen = j.turnOpen;
|
|
528
|
+
M.ticks = j.ticks || []; M.toolStats = j.toolStats || []; M.turnsList = j.turnsList || [];
|
|
529
|
+
M.approvalsList = j.approvalsList || []; M.changes = j.changes || []; M.delivs = j.delivs || [];
|
|
530
|
+
M.subagents = j.subagents || 0; M.approvals = j.approvals || 0; M.todos = j.todos || 0;
|
|
531
|
+
M.session = j.session || {}; M.bridge = j.bridge || {}; M.origin = j.origin || M.origin || ''; M.lastUpdate = Date.now();
|
|
532
|
+
M.model = j.model || '—';
|
|
533
|
+
if (j.replay && j.replay.on) {
|
|
534
|
+
RP.on = true; RP.mode = j.replay.mode || 'seq'; RP.playing = j.replay.playing; RP.speed = j.replay.speed;
|
|
535
|
+
RP.vtime = j.replay.vtime; RP.t0 = j.replay.t0; RP.t1 = j.replay.t1;
|
|
536
|
+
RP.pct = j.replay.pct; RP.done = j.replay.done; RP.anchor = Date.now();
|
|
537
|
+
RP.idx = j.replay.idx || 0; RP.total = j.replay.total || 0;
|
|
538
|
+
RP.stepMs = j.replay.stepMs || 110;
|
|
539
|
+
} else { RP.on = false; }
|
|
540
|
+
syncReplayUI();
|
|
541
|
+
var sel = $('pick');
|
|
542
|
+
if (sel && document.activeElement !== sel) sel.value = j.autoFollow ? '' : (M.session.id || '');
|
|
543
|
+
render();
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function pull() {
|
|
547
|
+
fetch(STATE_URL(), { cache: 'no-store' }).then(function (r) { return r.json() })
|
|
548
|
+
.then(applyState)
|
|
549
|
+
.catch(function () {
|
|
550
|
+
fails++;
|
|
551
|
+
M.ok = false; M.state = 'OFFLINE';
|
|
552
|
+
if (fails > 1) render();
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/* SSE:宿主那边变了才推一条;连不上就退回轮询 */
|
|
557
|
+
var ES = null, esMiss = 0, pollTimer = 0;
|
|
558
|
+
function startPolling() {
|
|
559
|
+
if (pollTimer) return;
|
|
560
|
+
(function loop() {
|
|
561
|
+
pull();
|
|
562
|
+
pollTimer = setTimeout(loop, RP.on ? 160 : 900);
|
|
563
|
+
})();
|
|
564
|
+
}
|
|
565
|
+
function startStream() {
|
|
566
|
+
if (!window.EventSource) { startPolling(); return }
|
|
567
|
+
try { ES = new EventSource(API_URL('events')) } catch (e) { startPolling(); return }
|
|
568
|
+
ES.onmessage = function (ev) {
|
|
569
|
+
try { applyState(JSON.parse(ev.data)) } catch (e) { }
|
|
570
|
+
};
|
|
571
|
+
ES.onopen = function () { esMiss = 0 };
|
|
572
|
+
ES.onerror = function () {
|
|
573
|
+
if (ES) { ES.close(); ES = null }
|
|
574
|
+
esMiss++;
|
|
575
|
+
if (esMiss <= 2) setTimeout(startStream, 1500);
|
|
576
|
+
else startPolling();
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/* ---------------- 表格 ---------------- */
|
|
581
|
+
function cell(v) { return '<span>' + (v == null ? '' : v) + '</span>' }
|
|
582
|
+
/* 用户输入的文本要转义,否则里面的 < > & 会把表格结构打坏 */
|
|
583
|
+
function esc(s) {
|
|
584
|
+
return String(s == null ? '' : s)
|
|
585
|
+
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
586
|
+
.replace(/"/g, '"');
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
/* ---------------- 多语言:中文是源,英文覆盖 ---------------- */
|
|
590
|
+
var EN = {
|
|
591
|
+
'轮次': 'Turns', '步骤': 'Steps', '工具调用': 'Tool calls', '本步耗时': 'Step time',
|
|
592
|
+
'技能': 'Skills', '本步': 'Step', '已加载': 'loaded', '工具': 'Tools', '用量': 'Usage',
|
|
593
|
+
'数据源:': 'Source: ', '实时': 'live', '前更新': 'ago', '条事件': 'events', '会话:': 'Session: ',
|
|
594
|
+
'自动 · 跟最新会话': 'Auto · newest session',
|
|
595
|
+
'会话逻辑 / 谁驱动了谁': 'SESSION FLOW / what drives what',
|
|
596
|
+
'上下文占用 / CONTEXT': 'CONTEXT',
|
|
597
|
+
'后台任务 / 工具在后台继续跑': 'JOBS / running in background',
|
|
598
|
+
'轮次时间线 / 这个会话怎么走过来的': 'TURNS / how this session went',
|
|
599
|
+
'工具统计 / 每种工具累计干了多少': 'TOOLS / total per kind',
|
|
600
|
+
'Event log / 真实事件': 'EVENT LOG / real events',
|
|
601
|
+
'全部': 'all', '运行中': 'running', '已结束': 'done', '有失败': 'has failures', '耗时 >1s': 'took >1s',
|
|
602
|
+
'环旋转:开': 'spin: on', '环旋转:关': 'spin: off',
|
|
603
|
+
'连线:节点连线': 'lines: ladders', '连线:中心光束': 'lines: beams', '连线:两者都有': 'lines: both',
|
|
604
|
+
'占比:次数': 'share: count', '占比:耗时': 'share: time',
|
|
605
|
+
'回到实时': 'back to live', '重播': 'replay', '暂停': 'pause', '继续': 'resume',
|
|
606
|
+
'↓ 因果': '↓ causal', '复用': 'load', '会话': 'Session', '任务': 'Job', '审批': 'Approval', '变更': 'Change',
|
|
607
|
+
'输入': 'in', '输出': 'out', '缓存': 'cache', '模型': 'model',
|
|
608
|
+
'类型': 'kind', '状态': 'state', '次数': 'count', '累计': 'total', '最近': 'last',
|
|
609
|
+
'调用次数': 'calls', '累计 / 平均': 'total / avg', '失败 / 最近': 'fail / last',
|
|
610
|
+
'最近几次调用': 'recent calls', '最近 6 次调用': 'last 6 calls',
|
|
611
|
+
'你的请求': 'your prompt', '步数 / 工具': 'steps / tools', '时长': 'duration', '交付': 'delivered',
|
|
612
|
+
'时间窗': 'window', '累计耗时': 'total time', '涉及轮次': 'turn', '这几步': 'steps',
|
|
613
|
+
'加载次数': 'loads', '最近使用': 'last used', '运行时长': 'ran for', '命令': 'command',
|
|
614
|
+
'时间': 'time', '结果': 'result', '轮次': 'turn',
|
|
615
|
+
'文件变更': 'file change', '交付文件': 'deliverable', '后台命令': 'command', '后台任务': 'job',
|
|
616
|
+
'执行命令': 'Command', '修改文件': 'Edit', '写入文件': 'Write', '读取文件': 'Read',
|
|
617
|
+
'查找文件': 'Glob', '搜索内容': 'Grep', '查看图片': 'Image', '抓取网页': 'Fetch',
|
|
618
|
+
'列后台任务': 'Jobs', '停止后台任务': 'Kill job', '加载技能': 'Skill', '交付文件 ': 'Present',
|
|
619
|
+
'(无文字请求)': '(no prompt)', '待定': 'pending', '进行中': 'running',
|
|
620
|
+
'用户': 'user', '名称': 'name', '详情': 'detail', '耗时': 'time', '成': 'ok', '层': 'layer',
|
|
621
|
+
'工作可视化': 'dsh session instrument', '⧉ 新窗口': '⧉ window', '环': 'rings', '数据源:未连接': 'Source: offline',
|
|
622
|
+
'① 会话': '① Session', '② 步骤': '② Steps', '③ 技能': '③ Skills', '④ 工具': '④ Tools',
|
|
623
|
+
'⑤ 任务': '⑤ Jobs', '⑥ 审批': '⑥ Approvals', '⑦ 变更': '⑦ Changes',
|
|
624
|
+
'▶ 重播': '▶ replay', '回到实时': 'back to live', '按轮分组 · 新在最上': 'grouped by turn · newest first',
|
|
625
|
+
|
|
626
|
+
}
|
|
627
|
+
var EN_RULES = [
|
|
628
|
+
[/^轮次 \/ (\d+)$/, 'Turns / $1'],
|
|
629
|
+
[/^本步 ([\d.]+s)$/, 'step $1'],
|
|
630
|
+
[/^(\d+)ms\/条$/, '$1ms/row'],
|
|
631
|
+
[/^数据源:(.+)$/, 'Source: $1'],
|
|
632
|
+
[/^重放 (.+)$/, 'replay $1'],
|
|
633
|
+
|
|
634
|
+
[/^(\d+) 步$/, '$1 steps'],
|
|
635
|
+
[/^(\d+) 工具$/, '$1 tools'],
|
|
636
|
+
[/^(\d+) 轮$/, '$1 turns'],
|
|
637
|
+
[/^共 (\d+) 条 · 新在最上$/, '$1 rows · newest first'],
|
|
638
|
+
[/^(\d+) 次 \/ (\d+) 种$/, '$1 calls / $2 kinds'],
|
|
639
|
+
[/^(\d+) 运行中 \/ 共 (\d+)$/, '$1 running / $2 total'],
|
|
640
|
+
[/^(\d+) 个$/, '$1'],
|
|
641
|
+
[/^(\d+) 步 · (\d+) 工具 · (.+)$/, '$1 steps · $2 tools · $3'],
|
|
642
|
+
[/^(\d+) 步 · (\d+) 工具$/, '$1 steps · $2 tools'],
|
|
643
|
+
[/^(\d+) 处$/, '$1 changes'],
|
|
644
|
+
[/^(\d+) 次$/, '$1'],
|
|
645
|
+
[/^运行 (\d+) · 共 (\d+)$/, 'running $1 · total $2'],
|
|
646
|
+
]
|
|
647
|
+
var LANG = (function () {
|
|
648
|
+
try {
|
|
649
|
+
var q = new URLSearchParams(location.search).get('lang');
|
|
650
|
+
if (q === 'zh' || q === 'en') return q;
|
|
651
|
+
var s = localStorage.getItem('dshviz.lang');
|
|
652
|
+
if (s === 'zh' || s === 'en') return s;
|
|
653
|
+
return /^zh/i.test(navigator.language || '') ? 'zh' : 'en';
|
|
654
|
+
} catch (e) { return 'zh' }
|
|
655
|
+
})();
|
|
656
|
+
var EN_FN = [
|
|
657
|
+
[/^连线轮数:(.+) \/ 共 (\d+) 轮$/, function (m) {
|
|
658
|
+
return 'line turns: ' + (m[1] === '全部' ? 'all' : m[1]) + ' / ' + m[2] + ' total'
|
|
659
|
+
}],
|
|
660
|
+
[/^显示轮数:(.+)$/, function (m) { return 'show turns: ' + (m[1] === '全部' ? 'all' : m[1]) }],
|
|
661
|
+
[/^实时 · ([\d.]+s) 前更新 · (\d+) 条事件 · preset (.+)$/, function (m) {
|
|
662
|
+
return 'live · updated ' + m[1] + ' ago · ' + m[2] + ' events · preset ' + m[3]
|
|
663
|
+
}],
|
|
664
|
+
[/^(.*)工作可视化$/, function (m) { return (m[1] || '') + 'dsh session instrument' }],
|
|
665
|
+
[/^共 (\d+) 条 · 按轮分组 · 新在最上$/, function (m) { return m[1] + ' rows · grouped by turn · newest first' }],
|
|
666
|
+
]
|
|
667
|
+
function tr(s) {
|
|
668
|
+
var k = String(s).trim();
|
|
669
|
+
if (!k) return s;
|
|
670
|
+
if (EN[k]) return s.replace(k, EN[k]);
|
|
671
|
+
for (var i = 0; i < EN_RULES.length; i++) {
|
|
672
|
+
if (EN_RULES[i][0].test(k)) return s.replace(k, k.replace(EN_RULES[i][0], EN_RULES[i][1]));
|
|
673
|
+
}
|
|
674
|
+
for (var j = 0; j < EN_FN.length; j++) {
|
|
675
|
+
var m2 = k.match(EN_FN[j][0]);
|
|
676
|
+
if (m2) return s.replace(k, EN_FN[j][1](m2));
|
|
677
|
+
}
|
|
678
|
+
return s;
|
|
679
|
+
}
|
|
680
|
+
function translateDom() {
|
|
681
|
+
if (LANG === 'zh') return;
|
|
682
|
+
var root = document.querySelector('.dash');
|
|
683
|
+
if (!root) return;
|
|
684
|
+
var w = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, null, false);
|
|
685
|
+
var nodes = [], n;
|
|
686
|
+
while ((n = w.nextNode())) nodes.push(n);
|
|
687
|
+
for (var i = 0; i < nodes.length; i++) {
|
|
688
|
+
var v = tr(nodes[i].nodeValue);
|
|
689
|
+
if (v !== nodes[i].nodeValue) nodes[i].nodeValue = v;
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
function render() {
|
|
694
|
+
/* 先按"显示轮数"收窄数据,后面所有面板都用这几个数组 */
|
|
695
|
+
var minT = scopeMinTurn();
|
|
696
|
+
M.sSteps = (minT === -Infinity) ? (M.steps || []) : (M.steps || []).filter(inScope);
|
|
697
|
+
M.sJobs = (minT === -Infinity) ? (M.jobs || []) : (M.jobs || []).filter(inScope);
|
|
698
|
+
M.sTools = (minT === -Infinity) ? (M.tools || []) : (M.tools || []).filter(inScope);
|
|
699
|
+
M.sLog = (minT === -Infinity) ? (M.log || []) : (M.log || []).filter(inScope);
|
|
700
|
+
M.sStats = SCOPE.turns ? aggTools(M.sTools) : (M.toolStats || []);
|
|
701
|
+
M.sAppr = (minT === -Infinity) ? (M.approvalsList || []) : (M.approvalsList || []).filter(inScope);
|
|
702
|
+
M.sChg = (minT === -Infinity) ? (M.changes || []) : (M.changes || []).filter(inScope);
|
|
703
|
+
M.sDeliv = (minT === -Infinity) ? (M.delivs || []) : (M.delivs || []).filter(inScope);
|
|
704
|
+
var curStep = (M.sSteps && M.sSteps.length) ? M.sSteps[M.sSteps.length - 1] : null;
|
|
705
|
+
var stepSec = curStep ? Math.max(0, ((curStep.end || clockNow()) - curStep.t) / 1000) : 0;
|
|
706
|
+
var st = { IDLE: '#8a919d', THINKING: '#f0a6c6', STREAMING: '#e6edfa', TOOL: '#ffc46b', OFFLINE: '#ff8a6b', 'NO DATA': '#ff8a6b', 'NO SESSION': '#ff8a6b' };
|
|
707
|
+
var c = M.ok ? (st[M.state] || '#8a919d') : '#ff8a6b';
|
|
708
|
+
$('tState').textContent = M.ok ? M.state : (M.state === 'OFFLINE' ? '未连接' : M.state);
|
|
709
|
+
$('tState').style.color = c;
|
|
710
|
+
$('cState').textContent = M.ok ? M.state : 'OFFLINE';
|
|
711
|
+
$('cState').style.color = c;
|
|
712
|
+
$('cTurn').textContent = M.turn;
|
|
713
|
+
$('tTurn').textContent = M.turn;
|
|
714
|
+
/* 会话进度条:一轮一段,宽度按该轮时长 */
|
|
715
|
+
var tlAll = M.turnsList || [];
|
|
716
|
+
var stripEl = $('tstrip');
|
|
717
|
+
if (stripEl) {
|
|
718
|
+
var totalDur = 0;
|
|
719
|
+
tlAll.forEach(function (x) { totalDur += Math.max(500, (x.end || clockNow()) - x.start) });
|
|
720
|
+
var sh = '';
|
|
721
|
+
tlAll.forEach(function (x) {
|
|
722
|
+
var d = Math.max(500, (x.end || clockNow()) - x.start);
|
|
723
|
+
var pct = totalDur ? (d / totalDur) * 100 : 0;
|
|
724
|
+
var cls = (x.no === M.turn && M.turnOpen) ? 'cur' : (/err|fail/i.test(x.reason || '') ? 'err' : '');
|
|
725
|
+
sh += '<i class="' + cls + '" style="flex:' + pct.toFixed(2) + ' 0 auto" title="T' + x.no + ' · ' +
|
|
726
|
+
Math.round(d / 1000) + 's"></i>';
|
|
727
|
+
});
|
|
728
|
+
stripEl.innerHTML = sh;
|
|
729
|
+
var lb = $('tTurnLb');
|
|
730
|
+
if (lb) lb.textContent = '轮次 / ' + tlAll.length;
|
|
731
|
+
}
|
|
732
|
+
$('tStep').textContent = M.step;
|
|
733
|
+
$('tTools').textContent = M.toolCount;
|
|
734
|
+
$('tTasks').textContent = stepSec.toFixed(1) + 's';
|
|
735
|
+
$('tSkills').textContent = M.skills.length;
|
|
736
|
+
$('tTitle').textContent = M.session.title ? M.session.title : (M.session.id || '未连接 bridge');
|
|
737
|
+
$('tClock').textContent = hhmmss();
|
|
738
|
+
|
|
739
|
+
var pct = Math.round((M.ctxPct || 0) * 100);
|
|
740
|
+
$('ctxBar').style.width = Math.min(100, pct) + '%';
|
|
741
|
+
$('ctxPct').textContent = pct + '%';
|
|
742
|
+
$('ctxTok').textContent = fmt(M.ctxUsed) + ' / ' + fmt(M.ctxWin);
|
|
743
|
+
$('tokIn').textContent = fmt(M.tok.in);
|
|
744
|
+
$('tokOut').textContent = fmt(M.tok.out);
|
|
745
|
+
$('tokCache').textContent = fmt(M.tok.cache);
|
|
746
|
+
$('tokModel').textContent = M.model || '—';
|
|
747
|
+
$('tokModel').title = M.model || '';
|
|
748
|
+
$('sTps').textContent = M.tps ? M.tps + ' tok/s' : '—';
|
|
749
|
+
|
|
750
|
+
/* 逻辑链 */
|
|
751
|
+
$('fS').textContent = M.turn;
|
|
752
|
+
$('fS2').textContent = 'step ' + M.step;
|
|
753
|
+
$('fA').textContent = M.step;
|
|
754
|
+
$('fA2').textContent = '本步 ' + stepSec.toFixed(1) + 's';
|
|
755
|
+
$('fK').textContent = M.skills.length;
|
|
756
|
+
$('fK2').textContent = '已加载';
|
|
757
|
+
$('fT').textContent = M.toolCount;
|
|
758
|
+
$('fT2').textContent = 'calls';
|
|
759
|
+
$('fO').textContent = fmt(M.tok.out);
|
|
760
|
+
$('fO2').textContent = 'in ' + fmt(M.tok.in);
|
|
761
|
+
|
|
762
|
+
/* 任务表:运行中优先,最多 6 行 */
|
|
763
|
+
var jobs = M.sJobs.slice().sort(function (a, b) {
|
|
764
|
+
return (b.state === 'RUN') - (a.state === 'RUN') || (b.t || 0) - (a.t || 0);
|
|
765
|
+
}).filter(function (j) {
|
|
766
|
+
if (FILTER.jobs === 'run') return j.state === 'RUN';
|
|
767
|
+
if (FILTER.jobs === 'done') return j.state !== 'RUN';
|
|
768
|
+
return true;
|
|
769
|
+
});
|
|
770
|
+
var h = '<div class="rw hdrow">' + cell('类型') + cell('任务') + cell('状态') + cell('时长') + '</div>';
|
|
771
|
+
jobs.forEach(function (j) {
|
|
772
|
+
h += '<div class="rw">' + cell(named(JOB_CN, j.kind)) +
|
|
773
|
+
cell(j.label) +
|
|
774
|
+
cell('<span class="' + (j.state === 'RUN' ? 'run' : 'done') + '">' +
|
|
775
|
+
(j.state === 'RUN' ? '运行中' : '已结束') + '</span>') +
|
|
776
|
+
cell(ago(j.end || j.t)) + '</div>';
|
|
777
|
+
});
|
|
778
|
+
if (!jobs.length) h += '<div class="rw"><span style="grid-column:1/-1;color:var(--dim)">— ' + (FILTER.jobs === 'all' ? '无后台任务' : '没有符合条件的任务') + '</span></div>';
|
|
779
|
+
$('taskTable').innerHTML = h;
|
|
780
|
+
$('sJobs').textContent = M.sJobs.filter(function (j) { return j.state === 'RUN'; }).length + ' 运行中 / 共 ' + M.sJobs.length +
|
|
781
|
+
(FILTER.jobs === 'all' ? '' : ' · 显示 ' + jobs.length);
|
|
782
|
+
|
|
783
|
+
/* 轮次时间线:最新在上,一轮两行 */
|
|
784
|
+
var tl = (M.turnsList || []).slice().reverse();
|
|
785
|
+
h = '';
|
|
786
|
+
tl.forEach(function (t3) {
|
|
787
|
+
var dur = ((t3.end || clockNow()) - t3.start) / 1000;
|
|
788
|
+
var durStr = dur >= 60 ? (dur / 60).toFixed(1) + 'm' : Math.round(dur) + 's';
|
|
789
|
+
var cur3 = (t3.no === M.turn && M.turnOpen);
|
|
790
|
+
var m2 = t3.steps + ' 步 · ' + t3.tools + ' 工具 · ' + durStr + (t3.files ? ' · ' + t3.files + ' 文件' : '');
|
|
791
|
+
h += '<div class="row2' + (cur3 ? ' cur' : '') + '">' +
|
|
792
|
+
'<div class="l1"><span class="no">T' + t3.no + (cur3 ? ' ●' : '') + '</span><span class="meta">' + m2 + '</span></div>' +
|
|
793
|
+
'<div class="ask">' + esc(t3.prompt || '(这一轮没有文字请求)') + '</div></div>';
|
|
794
|
+
});
|
|
795
|
+
if (!tl.length) h = '<div class="rw"><span style="color:var(--dim)">— 还没有轮次</span></div>';
|
|
796
|
+
$('turnTable').innerHTML = h;
|
|
797
|
+
$('sTurns').textContent = tl.length + ' 轮';
|
|
798
|
+
syncTurnBtns(); /* 轮次总数变了,让"连线轮数 x / 共 N 轮"跟着更新 */
|
|
799
|
+
|
|
800
|
+
/* 工具统计:常驻,按次数排序,不随窗口滚动 */
|
|
801
|
+
h = '<div class="rw hdrow">' + cell('工具') + cell('次数') + cell('累计') + cell('最近') + '</div>';
|
|
802
|
+
var stats = (M.sStats || []).filter(function (s) {
|
|
803
|
+
if (FILTER.tools === 'fail') return s.fail > 0;
|
|
804
|
+
if (FILTER.tools === 'slow') return s.ms >= 1000;
|
|
805
|
+
return true;
|
|
806
|
+
});
|
|
807
|
+
stats.forEach(function (s) {
|
|
808
|
+
h += '<div class="rw">' + cell('<span style="color:var(--tool)">' + named(TOOL_CN, s.name) + '</span>') +
|
|
809
|
+
cell('<span style="text-align:right">' + s.count + (s.fail ? '<span class="bad"> ×' + s.fail + '</span>' : '') + '</span>') +
|
|
810
|
+
cell('<span style="text-align:right;color:var(--dim)">' + (s.ms >= 1000 ? (s.ms / 1000).toFixed(1) + 's' : s.ms + 'ms') + '</span>') +
|
|
811
|
+
cell('<span style="text-align:right;color:var(--dim)">' + ago(s.last) + '</span>') + '</div>';
|
|
812
|
+
});
|
|
813
|
+
if (!stats.length) h += '<div class="rw"><span style="grid-column:1/-1;color:var(--dim)">— ' + (FILTER.tools === 'all' ? '还没有工具调用' : '没有符合条件的工具') + '</span></div>';
|
|
814
|
+
$('callTable').innerHTML = h;
|
|
815
|
+
$('sCall').textContent = (M.sTools ? M.sTools.length : 0) + ' 次 / ' + (M.sStats ? M.sStats.length : 0) + ' 种' +
|
|
816
|
+
(FILTER.tools === 'all' ? '' : ' · 显示 ' + stats.length);
|
|
817
|
+
|
|
818
|
+
/* 事件表:按轮分组,每组表头写你当时的请求 */
|
|
819
|
+
h = '<div class="rw hdrow">' + cell('时间') + cell('层') + cell('名称') +
|
|
820
|
+
cell('详情') + cell('耗时') + cell('成') + '</div>';
|
|
821
|
+
var rows = M.sLog.filter(function (e) { return FILTER.log === 'all' || e.layer === FILTER.log; }).slice().reverse();
|
|
822
|
+
var tMap = {};
|
|
823
|
+
(M.turnsList || []).forEach(function (t4) { tMap[t4.no] = t4; });
|
|
824
|
+
var seenTurn = null;
|
|
825
|
+
rows.forEach(function (e) {
|
|
826
|
+
var layerCn = { session: '会话', user: '用户', tool: '工具', task: '任务', skill: '技能', state: '状态' }[e.layer] || e.layer;
|
|
827
|
+
if (e.turn !== seenTurn) {
|
|
828
|
+
seenTurn = e.turn;
|
|
829
|
+
var tr4 = tMap[e.turn];
|
|
830
|
+
h += '<div class="tsep"><span>T' + e.turn +
|
|
831
|
+
(tr4 ? ' · ' + esc(tr4.prompt || '') : '') + '</span>' +
|
|
832
|
+
'<span>' + (tr4 ? tr4.steps + ' 步 · ' + tr4.tools + ' 工具' : '') + '</span></div>';
|
|
833
|
+
}
|
|
834
|
+
h += '<div class="rw">' + cell(hhmmss(e.atMs || 0)) +
|
|
835
|
+
cell('<span class="ly ' + e.layer + '">' + layerCn + '</span>') +
|
|
836
|
+
cell('<span class="nm">' + esc(e.name) + '</span>') +
|
|
837
|
+
cell(esc(e.detail || '')) +
|
|
838
|
+
cell('<span style="text-align:right">' + (e.ms == null ? '—' : e.ms) + '</span>') +
|
|
839
|
+
cell('<span class="' + (e.ok ? 'run' : 'bad') + '">' + (e.ok ? '·' : '×') + '</span>') + '</div>';
|
|
840
|
+
});
|
|
841
|
+
if (!rows.length) h += '<div class="rw"><span style="grid-column:1/-1;color:var(--dim)">— 暂无事件</span></div>';
|
|
842
|
+
$('rows').innerHTML = h;
|
|
843
|
+
$('logHint').textContent = '共 ' + M.sLog.length + ' 条' + (FILTER.log === 'all' ? '' : ' · 显示 ' + rows.length) + ' · 按轮分组 · 新在最上';
|
|
844
|
+
|
|
845
|
+
/* 数据来源 */
|
|
846
|
+
$('lkSrc').textContent = M.ok
|
|
847
|
+
? '数据源:' + (M.session.id || '?').replace('session-', 'session-').slice(0, 20) + '… · ' + fmt(M.bridge.bytes) + 'B'
|
|
848
|
+
: '数据源:未连接';
|
|
849
|
+
if (LANG !== 'zh') setTimeout(translateDom, 0); /* 英文模式:等这一帧所有赋值完成再换标签 */
|
|
850
|
+
$('lkMeta').innerHTML = M.ok
|
|
851
|
+
? ((RP.on
|
|
852
|
+
? (RP.mode === 'seq'
|
|
853
|
+
? '重放 ' + RP.stepMs + 'ms/条 · ' + (RP.idx + 1) + '/' + RP.total + ' · '
|
|
854
|
+
: '重放 ' + RP.speed + '× · ' + Math.round(RP.pct * 100) + '% · ')
|
|
855
|
+
: '实时 · ') +
|
|
856
|
+
((Date.now() - M.lastUpdate) / 1000).toFixed(1) + 's 前更新 · ' + (M.bridge.events || 0) + ' 条事件 · preset ' + (M.session.preset || '?'))
|
|
857
|
+
: '<span class="bad">' + esc(M.err || '连不上数据源:检查插件是否启用、或 bridge 是否在跑') + '</span>';
|
|
858
|
+
|
|
859
|
+
if (DEBUG) {
|
|
860
|
+
window.__M = M; /* 调试用:状态镜像 */
|
|
861
|
+
window.__HIT = HIT; /* 调试用:命中表(每帧重建) */
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
/* ---------------- 画布 ---------------- */
|
|
866
|
+
/* 调试面:只有 ?debug=1 / #debug / localStorage.dshviz.debug=1 时才挂到 window 上 */
|
|
867
|
+
var DEBUG = (function () {
|
|
868
|
+
try {
|
|
869
|
+
if (/[?&]debug=1\b/.test(location.search)) return true;
|
|
870
|
+
if (/(^|[#&])debug(=1)?\b/.test(location.hash)) return true;
|
|
871
|
+
return localStorage.getItem('dshviz.debug') === '1';
|
|
872
|
+
} catch (e) { return false; }
|
|
873
|
+
})();
|
|
874
|
+
var net = $('net'), nx = net.getContext('2d');
|
|
875
|
+
var VW = 0, VH = 0, DPR = 1;
|
|
876
|
+
var G = { ox: 0, oy: 0, base: 0, rad: [0, 0, 0, 0] };
|
|
877
|
+
var webC = null, webR = 0, pulse = 0, lastToolCount = -1;
|
|
878
|
+
|
|
879
|
+
|
|
880
|
+
/* 工具按类型着色:刻意避开 ① 白、② 粉(#f0a6c6)、③ 紫(#b28cff),否则会和上层环混在一起
|
|
881
|
+
命令=琥珀(工具环的主色)、写=橙、读=薄荷、网络=蓝、技能类=天蓝、其他=冷灰 */
|
|
882
|
+
function toolColor(name, a) {
|
|
883
|
+
var c = '#a8b4c4';
|
|
884
|
+
if (/^(read|glob|grep|ls)/.test(name) || name === 'read_image') c = '#7fe3c0';
|
|
885
|
+
else if (/^(write|edit)/.test(name)) c = '#ff7a5c';
|
|
886
|
+
else if (/^(bash|pwsh|job_|terminal)/.test(name)) c = '#ffc46b';
|
|
887
|
+
else if (/^(web_|cordis_)/.test(name)) c = '#8fb8ff';
|
|
888
|
+
else if (/^(skill|subagent)/.test(name)) c = '#8fd0e8';
|
|
889
|
+
return rgba(c, a);
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
function rgba(hex, a) {
|
|
893
|
+
var n = parseInt(hex.slice(1), 16);
|
|
894
|
+
return 'rgba(' + ((n >> 16) & 255) + ',' + ((n >> 8) & 255) + ',' + (n & 255) + ',' + a + ')';
|
|
895
|
+
}
|
|
896
|
+
function measure() {
|
|
897
|
+
var z = document.querySelector('.rings').getBoundingClientRect();
|
|
898
|
+
G.ox = z.left + z.width / 2;
|
|
899
|
+
G.oy = z.top + z.height / 2;
|
|
900
|
+
var R = Math.min(z.width * 0.40, z.height * 0.46);
|
|
901
|
+
G.base = R / 0.66;
|
|
902
|
+
G.rad = [0, 0, 0, 0, 0, 0, 0];
|
|
903
|
+
}
|
|
904
|
+
function buildWeb() {
|
|
905
|
+
var Rc = G.base * 0.245;
|
|
906
|
+
if (!(Rc > 24)) return;
|
|
907
|
+
webR = Rc;
|
|
908
|
+
var d = Math.min(DPR, 1.5);
|
|
909
|
+
webC = document.createElement('canvas');
|
|
910
|
+
webC.width = Math.ceil(2 * Rc * d); webC.height = Math.ceil(2 * Rc * d);
|
|
911
|
+
var c = webC.getContext('2d');
|
|
912
|
+
c.setTransform(d, 0, 0, d, 0, 0); c.translate(Rc, Rc);
|
|
913
|
+
/* 原来的 620 条随机曲线太乱,去掉;只留一层很淡的点,作为核心的"颗粒感" */
|
|
914
|
+
for (var i = 0; i < 26; i++) {
|
|
915
|
+
var na = Math.random() * TAU, nr = Rc * Math.random() * 0.95;
|
|
916
|
+
c.fillStyle = 'rgba(240,246,255,' + (0.15 + Math.random() * 0.5).toFixed(2) + ')';
|
|
917
|
+
c.beginPath(); c.arc(Math.cos(na) * nr, Math.sin(na) * nr, 0.7 + Math.random() * 1.1, 0, TAU); c.fill();
|
|
918
|
+
}
|
|
919
|
+
var g = c.createRadialGradient(0, 0, 0, 0, 0, Rc);
|
|
920
|
+
g.addColorStop(0, 'rgba(170,200,245,.15)');
|
|
921
|
+
g.addColorStop(0.55, 'rgba(150,170,205,.05)');
|
|
922
|
+
g.addColorStop(1, 'rgba(0,0,0,0)');
|
|
923
|
+
c.fillStyle = g; c.beginPath(); c.arc(0, 0, Rc, 0, TAU); c.fill();
|
|
924
|
+
c.strokeStyle = 'rgba(200,215,240,.16)'; c.lineWidth = 1;
|
|
925
|
+
c.beginPath(); c.arc(0, 0, Rc, 0, TAU); c.stroke();
|
|
926
|
+
}
|
|
927
|
+
function resize() {
|
|
928
|
+
DPR = Math.min(window.devicePixelRatio || 1, 2);
|
|
929
|
+
VW = window.innerWidth; VH = window.innerHeight;
|
|
930
|
+
net.width = Math.round(VW * DPR); net.height = Math.round(VH * DPR);
|
|
931
|
+
net.style.width = VW + 'px'; net.style.height = VH + 'px';
|
|
932
|
+
nx.setTransform(DPR, 0, 0, DPR, 0, 0);
|
|
933
|
+
measure(); buildWeb();
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
function draw(t) {
|
|
937
|
+
var X = nx;
|
|
938
|
+
X2 = nx;
|
|
939
|
+
HIT.items = [];
|
|
940
|
+
X.clearRect(0, 0, VW, VH);
|
|
941
|
+
if (!G.base) return;
|
|
942
|
+
var ox = G.ox, oy = G.oy, R = G.rad, b = pulse;
|
|
943
|
+
var beat = (Math.sin(t * 1.5) + 1) * 0.5;
|
|
944
|
+
|
|
945
|
+
/* 网:内核 */
|
|
946
|
+
if (webC) {
|
|
947
|
+
X.save(); X.translate(ox, oy); X.rotate(t * 0.006);
|
|
948
|
+
X.globalAlpha = 0.5 + b * 0.4;
|
|
949
|
+
X.drawImage(webC, -webR, -webR, webR * 2, webR * 2);
|
|
950
|
+
X.restore();
|
|
951
|
+
}
|
|
952
|
+
var stc = { IDLE: '#8a919d', THINKING: '#f0a6c6', STREAMING: '#e6edfa', TOOL: '#ffc46b' }[M.state] || '#8a919d';
|
|
953
|
+
if (!M.ok) stc = '#ff8a6b';
|
|
954
|
+
var cr = 3 + beat * 2 + b * 3;
|
|
955
|
+
var grd = X.createRadialGradient(ox, oy, 0, ox, oy, Math.max(26, G.base * 0.16));
|
|
956
|
+
grd.addColorStop(0, rgba(stc, 0.5 + b * 0.3));
|
|
957
|
+
grd.addColorStop(1, rgba(stc, 0));
|
|
958
|
+
X.fillStyle = grd; X.beginPath(); X.arc(ox, oy, Math.max(26, G.base * 0.16), 0, TAU); X.fill();
|
|
959
|
+
X.fillStyle = '#fff'; X.beginPath(); X.arc(ox, oy, cr, 0, TAU); X.fill();
|
|
960
|
+
|
|
961
|
+
var i, rr, a;
|
|
962
|
+
|
|
963
|
+
/* 可见环数决定半径:环少 → 摊开;环多 → 收紧 */
|
|
964
|
+
var visIdx = [];
|
|
965
|
+
for (i = 0; i < RINGVIS.length; i++) if (RINGVIS[i]) visIdx.push(i);
|
|
966
|
+
var RMAX = G.base * 0.66, RMIN = G.base * 0.24;
|
|
967
|
+
if (visIdx.length === 1) R[visIdx[0]] = RMAX * 0.62;
|
|
968
|
+
else if (visIdx.length > 1) {
|
|
969
|
+
for (var vk = 0; vk < visIdx.length; vk++) {
|
|
970
|
+
R[visIdx[vk]] = RMIN + (RMAX - RMIN) * vk / (visIdx.length - 1);
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
/* 隐藏的环沿用内侧最近可见环的半径,避免取到过期的值 */
|
|
974
|
+
var carryR = RMIN * 0.7;
|
|
975
|
+
for (i = 0; i < RINGVIS.length; i++) { if (RINGVIS[i]) carryR = R[i]; else R[i] = carryR; }
|
|
976
|
+
|
|
977
|
+
/* ---- 先把连线画在环的下面(用上一帧的几何) ---- */
|
|
978
|
+
if (CHAIN_PREV.length) {
|
|
979
|
+
for (var cq = 0; cq < CHAIN_PREV.length; cq++) {
|
|
980
|
+
var ce = CHAIN_PREV[cq], cps = ce.pts;
|
|
981
|
+
for (var cp = 1; cp < cps.length; cp++) segLine(cps[cp - 1], cps[cp], ce.bright, ce.fr);
|
|
982
|
+
/* 移动光点:按路径长度插值 */
|
|
983
|
+
var lens = [], tot = 0;
|
|
984
|
+
for (var cl = 1; cl < cps.length; cl++) {
|
|
985
|
+
var ldx = cps[cl].x - cps[cl - 1].x, ldy = cps[cl].y - cps[cl - 1].y;
|
|
986
|
+
var lL = Math.sqrt(ldx * ldx + ldy * ldy);
|
|
987
|
+
lens.push(lL); tot += lL;
|
|
988
|
+
}
|
|
989
|
+
if (tot > 4) {
|
|
990
|
+
var cfrac = ((t * 0.55 + ce.phase) % 1 + 1) % 1;
|
|
991
|
+
var want2 = cfrac * tot, acc3 = 0, dotP = cps[cps.length - 1];
|
|
992
|
+
for (var ck = 0; ck < lens.length; ck++) {
|
|
993
|
+
if (acc3 + lens[ck] >= want2) {
|
|
994
|
+
var uu = lens[ck] ? (want2 - acc3) / lens[ck] : 0;
|
|
995
|
+
dotP = {
|
|
996
|
+
x: cps[ck].x + (cps[ck + 1].x - cps[ck].x) * uu,
|
|
997
|
+
y: cps[ck].y + (cps[ck + 1].y - cps[ck].y) * uu
|
|
998
|
+
};
|
|
999
|
+
break;
|
|
1000
|
+
}
|
|
1001
|
+
acc3 += lens[ck];
|
|
1002
|
+
}
|
|
1003
|
+
var cA2 = ce.bright ? (0.55 + 0.45 * Math.sin(cfrac * Math.PI)) : 0.28;
|
|
1004
|
+
if (ce.bright) {
|
|
1005
|
+
var dg2 = X.createRadialGradient(dotP.x, dotP.y, 0, dotP.x, dotP.y, 9);
|
|
1006
|
+
dg2.addColorStop(0, 'rgba(255,250,235,' + (0.5 * cA2).toFixed(3) + ')');
|
|
1007
|
+
dg2.addColorStop(1, 'rgba(255,214,150,0)');
|
|
1008
|
+
X.fillStyle = dg2; X.beginPath(); X.arc(dotP.x, dotP.y, 9, 0, TAU); X.fill();
|
|
1009
|
+
}
|
|
1010
|
+
X.beginPath(); X.arc(dotP.x, dotP.y, ce.bright ? 2.1 : 1.3, 0, TAU);
|
|
1011
|
+
X.fillStyle = 'rgba(255,250,238,' + cA2.toFixed(3) + ')'; X.fill();
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
CHAIN_PREV = [];
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
/* 连线只画最近 N 轮;把这个集合算一次,②环的"有没有线"和连线本身都用它 */
|
|
1018
|
+
var drawTurns = [];
|
|
1019
|
+
(function () {
|
|
1020
|
+
var list = M.sSteps || [];
|
|
1021
|
+
var limit = CN.turns || 999;
|
|
1022
|
+
for (var q = list.length - 1; q >= 0 && drawTurns.length < limit; q--) {
|
|
1023
|
+
if (drawTurns.indexOf(list[q].turn) < 0) drawTurns.push(list[q].turn);
|
|
1024
|
+
}
|
|
1025
|
+
})();
|
|
1026
|
+
|
|
1027
|
+
/* ① 会话环:context 进度 + turn 刻度;不转 */
|
|
1028
|
+
if (RINGVIS[0]) {
|
|
1029
|
+
rr = R[0];
|
|
1030
|
+
X.beginPath(); X.arc(ox, oy, rr, 0, TAU);
|
|
1031
|
+
X.strokeStyle = rgba(COL.session, 0.30); X.lineWidth = 3.2; X.stroke();
|
|
1032
|
+
var pa = -Math.PI / 2 + TAU * Math.min(0.995, M.ctxPct || 0);
|
|
1033
|
+
X.beginPath(); X.arc(ox, oy, rr, -Math.PI / 2, pa);
|
|
1034
|
+
X.strokeStyle = rgba(COL.session, 0.20); X.lineWidth = 9; X.stroke();
|
|
1035
|
+
X.strokeStyle = rgba(COL.session, 0.96); X.lineWidth = 3.2; X.stroke();
|
|
1036
|
+
X.fillStyle = '#fff';
|
|
1037
|
+
X.beginPath(); X.arc(ox + Math.cos(pa) * rr, oy + Math.sin(pa) * rr, 2.4, 0, TAU); X.fill();
|
|
1038
|
+
/* 12 个刻度 = 最近 12 轮(会话长了也不能假称"第 1~12 轮") */
|
|
1039
|
+
var lastTurnNo = Math.max(1, M.turn || 1);
|
|
1040
|
+
var firstTickTurn = Math.max(1, lastTurnNo - 11);
|
|
1041
|
+
for (i = 0; i < 12; i++) {
|
|
1042
|
+
a = -Math.PI / 2 + TAU * i / 12;
|
|
1043
|
+
var tickTurn = firstTickTurn + i;
|
|
1044
|
+
var past = tickTurn <= lastTurnNo;
|
|
1045
|
+
X.beginPath();
|
|
1046
|
+
X.moveTo(ox + Math.cos(a) * (rr + 9), oy + Math.sin(a) * (rr + 9));
|
|
1047
|
+
X.lineTo(ox + Math.cos(a) * (rr + 16), oy + Math.sin(a) * (rr + 16));
|
|
1048
|
+
X.lineWidth = 1; X.strokeStyle = rgba(COL.session, past ? 0.55 : 0.13); X.stroke();
|
|
1049
|
+
hitReg({
|
|
1050
|
+
kind: 'turn', r: rr + 12, a0: a - 0.035, a1: a + 0.035,
|
|
1051
|
+
label: '轮次 ' + tickTurn, sub: past ? '已发生' : '未到', data: { no: tickTurn }
|
|
1052
|
+
});
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
/* ② 步骤环:整场的每一步都在,按时间分格聚合 —— 格长/格粗 = 那段时间累计花了多久 */
|
|
1057
|
+
if (RINGVIS[1]) {
|
|
1058
|
+
rr = R[1];
|
|
1059
|
+
X.beginPath(); X.arc(ox, oy, rr, 0, TAU);
|
|
1060
|
+
X.strokeStyle = rgba(COL.task, 0.20); X.lineWidth = 1; X.stroke();
|
|
1061
|
+
var stp = M.sSteps || [];
|
|
1062
|
+
if (stp.length) {
|
|
1063
|
+
var nb = Math.max(24, Math.min(60, stp.length));
|
|
1064
|
+
var spanS = Math.max(120000, clockNow() - stp[0].t);
|
|
1065
|
+
R2GEO = { t0: stp[0].t, span: spanS, nb: nb };
|
|
1066
|
+
var buckets = [], bi, bucketLine = [];
|
|
1067
|
+
for (i = 0; i < nb; i++) { buckets.push(0); bucketLine.push(false); }
|
|
1068
|
+
for (i = 0; i < stp.length; i++) {
|
|
1069
|
+
var sd = Math.max(0, (stp[i].end || clockNow()) - stp[i].t);
|
|
1070
|
+
bi = Math.min(nb - 1, Math.floor((stp[i].t - stp[0].t) / spanS * nb));
|
|
1071
|
+
buckets[bi] += sd;
|
|
1072
|
+
if (!bucketLine[bi] && drawTurns.indexOf(stp[i].turn) >= 0) bucketLine[bi] = true;
|
|
1073
|
+
}
|
|
1074
|
+
var maxB = 1;
|
|
1075
|
+
for (i = 0; i < nb; i++) if (buckets[i] > maxB) maxB = buckets[i];
|
|
1076
|
+
var slotB = TAU / nb;
|
|
1077
|
+
for (i = 0; i < nb; i++) {
|
|
1078
|
+
var fr = buckets[i] / maxB;
|
|
1079
|
+
if (fr <= 0.002) continue;
|
|
1080
|
+
var ageB = clockNow() - (stp[0].t + (i + 0.5) / nb * spanS);
|
|
1081
|
+
var aB = -Math.PI / 2 + (ageB / spanS) * TAU + rotOf(1, t); // 和 ④ 一致:越旧越靠顺时针
|
|
1082
|
+
var lenB = slotB * (0.2 + 0.75 * fr);
|
|
1083
|
+
/* 连线范围之外的格子画暗:告诉人"这些步骤没有线,因为不在最近 N 轮里" */
|
|
1084
|
+
var mulB = bucketLine[i] ? 1 : 0.3;
|
|
1085
|
+
X.beginPath(); X.arc(ox, oy, rr, aB - lenB, aB);
|
|
1086
|
+
X.strokeStyle = rgba(COL.task, (0.20 + 0.65 * fr) * mulB);
|
|
1087
|
+
X.lineWidth = 2 + 3.5 * fr;
|
|
1088
|
+
X.stroke();
|
|
1089
|
+
hitReg({
|
|
1090
|
+
kind: 'step', r: rr, a0: aB - lenB, a1: aB,
|
|
1091
|
+
label: '步骤 · ' + (buckets[i] / 1000).toFixed(1) + 's',
|
|
1092
|
+
sub: '',
|
|
1093
|
+
data: { t0: stp[0].t + i / nb * spanS, t1: stp[0].t + (i + 1) / nb * spanS }
|
|
1094
|
+
});
|
|
1095
|
+
}
|
|
1096
|
+
/* 正在跑的那一步:12 点方向点一颗亮头 */
|
|
1097
|
+
if (M.turnOpen) {
|
|
1098
|
+
X.beginPath(); X.arc(ox, oy, rr, -Math.PI / 2 - 0.035 + rotOf(1, t), -Math.PI / 2 + 0.035 + rotOf(1, t));
|
|
1099
|
+
X.strokeStyle = rgba(COL.task, 0.95); X.lineWidth = 6; X.stroke();
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
/* ③ 技能环:16 个稀疏刻度,加载一个亮一个(技能是稀疏事件,一亮就代表"用过") */
|
|
1105
|
+
if (RINGVIS[2]) {
|
|
1106
|
+
rr = R[2];
|
|
1107
|
+
X.beginPath(); X.arc(ox, oy, rr, 0, TAU);
|
|
1108
|
+
X.strokeStyle = rgba(COL.skill, 0.22); X.lineWidth = 1; X.stroke();
|
|
1109
|
+
if (M.skills.length) {
|
|
1110
|
+
var sIdx = Math.min(15, Math.max(0, M.skills.length - 1));
|
|
1111
|
+
SKILL_ANG = -Math.PI / 2 + TAU * sIdx / 16 + rotOf(2, t);
|
|
1112
|
+
}
|
|
1113
|
+
for (i = 0; i < 16; i++) {
|
|
1114
|
+
a = rotOf(2, t) - Math.PI / 2 + TAU * i / 16;
|
|
1115
|
+
var on = i < M.skills.length;
|
|
1116
|
+
X.beginPath();
|
|
1117
|
+
X.moveTo(ox + Math.cos(a) * (rr - 3), oy + Math.sin(a) * (rr - 3));
|
|
1118
|
+
X.lineTo(ox + Math.cos(a) * (rr + (on ? 13 : 7)), oy + Math.sin(a) * (rr + (on ? 13 : 7)));
|
|
1119
|
+
X.lineWidth = on ? 2 : 1;
|
|
1120
|
+
X.strokeStyle = rgba(COL.skill, on ? 0.92 : 0.14);
|
|
1121
|
+
X.stroke();
|
|
1122
|
+
if (on) {
|
|
1123
|
+
X.beginPath(); X.arc(ox + Math.cos(a) * (rr + 16), oy + Math.sin(a) * (rr + 16), 1.9, 0, TAU);
|
|
1124
|
+
X.fillStyle = rgba(COL.skill, 0.95); X.fill();
|
|
1125
|
+
var sk = M.skills[i];
|
|
1126
|
+
hitReg({
|
|
1127
|
+
kind: 'skill', r: rr, a0: a - 0.05, a1: a + 0.05,
|
|
1128
|
+
label: (sk ? (cn(SKILL_CN, sk.name) || sk.name) : '技能'),
|
|
1129
|
+
sub: sk ? sk.name : '', data: { i: i }
|
|
1130
|
+
});
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
/* ④ 工具环:不看时间,看「构成」
|
|
1136
|
+
每种工具占一段弧,弧长 ∝ 调用次数,颜色区分类型。
|
|
1137
|
+
想知道"什么时候调的"看右栏最近调用和底部事件表,环只回答"用了哪些、各多少" */
|
|
1138
|
+
rr = R[3];
|
|
1139
|
+
if (RINGVIS[3]) {
|
|
1140
|
+
X.beginPath(); X.arc(ox, oy, rr, 0, TAU);
|
|
1141
|
+
X.strokeStyle = 'rgba(200,215,240,.12)'; X.lineWidth = 1; X.stroke();
|
|
1142
|
+
}
|
|
1143
|
+
var tn = clockNow();
|
|
1144
|
+
var tstats = (M.sStats || []).filter(function (s) { return s.count > 0; });
|
|
1145
|
+
/* 占比口径:次数 / 耗时;并且每段至少 3°,否则只调过一次的工具会缩成 1° 看不见 */
|
|
1146
|
+
var useMs = (CN.share === 'ms');
|
|
1147
|
+
var vals = tstats.map(function (s) { return useMs ? Math.max(s.ms, 1) : s.count; });
|
|
1148
|
+
var vtotal = vals.reduce(function (a, v) { return a + v; }, 0) || 1;
|
|
1149
|
+
var segAngles;
|
|
1150
|
+
var MINS = 0.052; /* ≈3° */
|
|
1151
|
+
if (tstats.length && tstats.length * MINS < TAU) {
|
|
1152
|
+
var free = TAU - tstats.length * MINS;
|
|
1153
|
+
segAngles = vals.map(function (v) { return MINS + free * v / vtotal; });
|
|
1154
|
+
} else {
|
|
1155
|
+
segAngles = tstats.map(function () { return TAU / Math.max(1, tstats.length); });
|
|
1156
|
+
}
|
|
1157
|
+
var segMap = {};
|
|
1158
|
+
var aCur = -Math.PI / 2 + rotOf(3, t);
|
|
1159
|
+
for (i = 0; i < tstats.length; i++) {
|
|
1160
|
+
var segLen = segAngles[i];
|
|
1161
|
+
var gap = Math.min(0.03, segLen * 0.18);
|
|
1162
|
+
var a0s = aCur + gap, a1s = aCur + segLen - gap;
|
|
1163
|
+
if (RINGVIS[3]) {
|
|
1164
|
+
X.beginPath(); X.arc(ox, oy, rr, a0s, a1s);
|
|
1165
|
+
X.strokeStyle = toolColor(tstats[i].name, 0.92);
|
|
1166
|
+
X.lineWidth = 11; X.stroke();
|
|
1167
|
+
}
|
|
1168
|
+
segMap[tstats[i].name] = (a0s + a1s) / 2;
|
|
1169
|
+
/* 环藏起来了就别登记命中,免得点在看不见的地方还弹卡片;segMap 仍保留给连线用 */
|
|
1170
|
+
if (RINGVIS[3]) hitReg({
|
|
1171
|
+
kind: 'tool', r: rr, a0: a0s, a1: a1s,
|
|
1172
|
+
label: cn(TOOL_CN, tstats[i].name) || tstats[i].name, sub: tstats[i].name,
|
|
1173
|
+
data: { name: tstats[i].name }
|
|
1174
|
+
});
|
|
1175
|
+
aCur += segLen;
|
|
1176
|
+
}
|
|
1177
|
+
HIT.ox = ox; HIT.oy = oy;
|
|
1178
|
+
/* 正在跑的那类工具:在它的段上点个亮头 */
|
|
1179
|
+
var live = (M.sTools || []).filter(function (x) { return x.ms == null; }).slice(-6);
|
|
1180
|
+
for (i = 0; i < live.length; i++) {
|
|
1181
|
+
var la = segMap[live[i].name];
|
|
1182
|
+
if (la == null) continue;
|
|
1183
|
+
if (RINGVIS[3]) {
|
|
1184
|
+
X.beginPath(); X.arc(ox, oy, rr, la - 0.05, la + 0.05);
|
|
1185
|
+
X.strokeStyle = 'rgba(255,255,255,.85)'; X.lineWidth = 15; X.stroke();
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
/* ⑤ 任务环:后台任务,弧长 ∝ 运行时长,颜色 = 状态 */
|
|
1190
|
+
if (RINGVIS[4]) {
|
|
1191
|
+
var rr5 = R[4];
|
|
1192
|
+
X.beginPath(); X.arc(ox, oy, rr5, 0, TAU);
|
|
1193
|
+
X.strokeStyle = rgba(RING_COL[4], 0.14); X.lineWidth = 1; X.stroke();
|
|
1194
|
+
var jl5 = (M.sJobs || []).map(function (j) {
|
|
1195
|
+
var jend = j.end || (j.state === 'RUN' ? clockNow() : j.t);
|
|
1196
|
+
return { j: j, d: Math.max(1, jend - j.t) };
|
|
1197
|
+
}).sort(function (a2, b2) { return b2.d - a2.d; }).slice(0, 200);
|
|
1198
|
+
var jdS = jl5.reduce(function (a2, x2) { return a2 + x2.d; }, 0) || 1;
|
|
1199
|
+
var JM = 0.004, jFree = Math.max(0, TAU - jl5.length * JM), aJ = -Math.PI / 2 + rotOf(4, t);
|
|
1200
|
+
for (i = 0; i < jl5.length; i++) {
|
|
1201
|
+
var sJ2 = JM + jFree * jl5[i].d / jdS;
|
|
1202
|
+
var stJ2 = String(jl5[i].j.state || '');
|
|
1203
|
+
var cJ2 = stJ2 === 'RUN' ? 'rgba(127,216,196,.95)'
|
|
1204
|
+
: /fail|error/i.test(stJ2) ? 'rgba(255,138,107,.9)'
|
|
1205
|
+
: /kill|cancel/i.test(stJ2) ? 'rgba(138,145,157,.8)'
|
|
1206
|
+
: 'rgba(127,216,196,.38)';
|
|
1207
|
+
X.beginPath(); X.arc(ox, oy, rr5, aJ + 0.003, aJ + sJ2 - 0.003);
|
|
1208
|
+
X.strokeStyle = cJ2; X.lineWidth = stJ2 === 'RUN' ? 7 : 4.5; X.stroke();
|
|
1209
|
+
var jj = jl5[i].j;
|
|
1210
|
+
JOBARC[jj.id] = aJ + sJ2 / 2;
|
|
1211
|
+
hitReg({
|
|
1212
|
+
kind: 'job', r: rr5, a0: aJ, a1: aJ + sJ2,
|
|
1213
|
+
label: (cn(JOB_CN, jj.kind) || '任务') + ' · ' + (jj.state || ''),
|
|
1214
|
+
sub: String(jj.label || '').slice(0, 40), data: { id: jj.id }
|
|
1215
|
+
});
|
|
1216
|
+
aJ += sJ2;
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
/* ⑥ 审批环:每次审批一根刻度,颜色 = 结果(绿=放行 / 红=拒绝 / 琥珀=待定) */
|
|
1221
|
+
if (RINGVIS[5]) {
|
|
1222
|
+
var rr6 = R[5];
|
|
1223
|
+
X.beginPath(); X.arc(ox, oy, rr6, 0, TAU);
|
|
1224
|
+
X.strokeStyle = rgba(RING_COL[5], 0.14); X.lineWidth = 1; X.stroke();
|
|
1225
|
+
var apl = (M.sAppr || []);
|
|
1226
|
+
var spanAp = Math.max(120000, apl.length ? (tn - apl[0].t) : 120000);
|
|
1227
|
+
RSPAN[5] = spanAp;
|
|
1228
|
+
for (i = 0; i < apl.length; i++) {
|
|
1229
|
+
var aA = -Math.PI / 2 + ((tn - apl[i].t) / spanAp) * TAU + rotOf(5, t);
|
|
1230
|
+
var oc = String(apl[i].outcome || '');
|
|
1231
|
+
var cA = /deny|reject|refus/i.test(oc) ? 'rgba(255,138,107,.95)'
|
|
1232
|
+
: /allow|approve|grant/i.test(oc) ? 'rgba(143,224,200,.95)'
|
|
1233
|
+
: 'rgba(255,196,107,.85)';
|
|
1234
|
+
X.beginPath();
|
|
1235
|
+
X.moveTo(ox + Math.cos(aA) * (rr6 - 6), oy + Math.sin(aA) * (rr6 - 6));
|
|
1236
|
+
X.lineTo(ox + Math.cos(aA) * (rr6 + 7), oy + Math.sin(aA) * (rr6 + 7));
|
|
1237
|
+
X.lineWidth = 2.2; X.strokeStyle = cA; X.stroke();
|
|
1238
|
+
X.beginPath(); X.arc(ox + Math.cos(aA) * (rr6 + 10), oy + Math.sin(aA) * (rr6 + 10), 1.6, 0, TAU);
|
|
1239
|
+
X.fillStyle = cA; X.fill();
|
|
1240
|
+
hitReg({
|
|
1241
|
+
kind: 'approval', r: rr6, a0: aA - 0.035, a1: aA + 0.035,
|
|
1242
|
+
label: '审批 · ' + (apl[i].tool || 'tool'),
|
|
1243
|
+
sub: apl[i].outcome || '待定', data: { i: i }
|
|
1244
|
+
});
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
/* ⑦ 变更环:文件改动 + 交付,每根刻度 = 一次改动 */
|
|
1249
|
+
if (RINGVIS[6]) {
|
|
1250
|
+
var rr7 = R[6];
|
|
1251
|
+
X.beginPath(); X.arc(ox, oy, rr7, 0, TAU);
|
|
1252
|
+
X.strokeStyle = rgba(RING_COL[6], 0.14); X.lineWidth = 1; X.stroke();
|
|
1253
|
+
var chg = (M.sChg || []).map(function (c) { return { t: c.t, turn: c.turn, big: false } })
|
|
1254
|
+
.concat((M.sDeliv || []).map(function (d2) { return { t: d2.t, turn: d2.turn, big: true } }))
|
|
1255
|
+
.sort(function (a3, b3) { return a3.t - b3.t; });
|
|
1256
|
+
var spanCh = Math.max(120000, chg.length ? (tn - chg[0].t) : 120000);
|
|
1257
|
+
RSPAN[6] = spanCh;
|
|
1258
|
+
for (i = 0; i < chg.length; i++) {
|
|
1259
|
+
var aC = -Math.PI / 2 + ((tn - chg[i].t) / spanCh) * TAU + rotOf(6, t);
|
|
1260
|
+
X.beginPath();
|
|
1261
|
+
X.moveTo(ox + Math.cos(aC) * (rr7 - (chg[i].big ? 9 : 4)), oy + Math.sin(aC) * (rr7 - (chg[i].big ? 9 : 4)));
|
|
1262
|
+
X.lineTo(ox + Math.cos(aC) * (rr7 + (chg[i].big ? 12 : 5)), oy + Math.sin(aC) * (rr7 + (chg[i].big ? 12 : 5)));
|
|
1263
|
+
X.lineWidth = chg[i].big ? 3 : 1.4;
|
|
1264
|
+
X.strokeStyle = chg[i].big ? 'rgba(255,196,107,.95)' : 'rgba(143,184,255,.8)';
|
|
1265
|
+
X.stroke();
|
|
1266
|
+
hitReg({
|
|
1267
|
+
kind: chg[i].big ? 'deliv' : 'change', r: rr7, a0: aC - 0.03, a1: aC + 0.03,
|
|
1268
|
+
label: chg[i].big ? '交付文件' : '文件变更', sub: 'T' + (chg[i].turn || '?'),
|
|
1269
|
+
data: { t: chg[i].t, turn: chg[i].turn, big: chg[i].big }
|
|
1270
|
+
});
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
/* ---- 光束:从核心射向「正在跑的那类工具」所在的扇区 ---- */
|
|
1275
|
+
var pulseK = 0.75 + 0.25 * Math.sin(t * 3.2);
|
|
1276
|
+
var stpAll = M.sSteps || [];
|
|
1277
|
+
var spanS2 = Math.max(120000, stpAll.length ? (tn - stpAll[0].t) : 120000);
|
|
1278
|
+
|
|
1279
|
+
function drawBeam(ang, outer, intensity) {
|
|
1280
|
+
var r0 = webR ? webR * 0.95 : G.base * 0.22;
|
|
1281
|
+
var r1 = outer;
|
|
1282
|
+
/* 扇形辉光:光束看到的是"一片光",不是一根线 */
|
|
1283
|
+
var wedge = X.createRadialGradient(ox, oy, r0, ox, oy, r1);
|
|
1284
|
+
wedge.addColorStop(0, 'rgba(255,214,150,0)');
|
|
1285
|
+
wedge.addColorStop(0.55, 'rgba(255,214,150,' + (0.05 * intensity).toFixed(3) + ')');
|
|
1286
|
+
wedge.addColorStop(1, 'rgba(255,236,205,' + (0.16 * intensity).toFixed(3) + ')');
|
|
1287
|
+
X.beginPath();
|
|
1288
|
+
X.moveTo(ox + Math.cos(ang - 0.075) * r0, oy + Math.sin(ang - 0.075) * r0);
|
|
1289
|
+
X.lineTo(ox + Math.cos(ang - 0.022) * r1, oy + Math.sin(ang - 0.022) * r1);
|
|
1290
|
+
X.lineTo(ox + Math.cos(ang + 0.022) * r1, oy + Math.sin(ang + 0.022) * r1);
|
|
1291
|
+
X.lineTo(ox + Math.cos(ang + 0.075) * r0, oy + Math.sin(ang + 0.075) * r0);
|
|
1292
|
+
X.closePath();
|
|
1293
|
+
X.fillStyle = wedge; X.fill();
|
|
1294
|
+
var x0 = ox + Math.cos(ang) * r0, y0 = oy + Math.sin(ang) * r0;
|
|
1295
|
+
var x1 = ox + Math.cos(ang) * r1, y1 = oy + Math.sin(ang) * r1;
|
|
1296
|
+
var g = X.createLinearGradient(x0, y0, x1, y1);
|
|
1297
|
+
g.addColorStop(0, 'rgba(255,214,150,0)');
|
|
1298
|
+
g.addColorStop(0.35, 'rgba(255,214,150,' + (0.10 * intensity).toFixed(3) + ')');
|
|
1299
|
+
g.addColorStop(1, 'rgba(255,246,230,' + (0.72 * intensity).toFixed(3) + ')');
|
|
1300
|
+
X.beginPath(); X.moveTo(x0, y0); X.lineTo(x1, y1);
|
|
1301
|
+
X.strokeStyle = g; X.lineWidth = 1.6 + 2.4 * intensity; X.stroke();
|
|
1302
|
+
var hg = X.createRadialGradient(x1, y1, 0, x1, y1, 15);
|
|
1303
|
+
hg.addColorStop(0, 'rgba(255,250,240,' + (0.45 * intensity).toFixed(3) + ')');
|
|
1304
|
+
hg.addColorStop(1, 'rgba(255,220,160,0)');
|
|
1305
|
+
X.fillStyle = hg; X.beginPath(); X.arc(x1, y1, 15, 0, TAU); X.fill();
|
|
1306
|
+
X.beginPath(); X.arc(x1, y1, 2.4, 0, TAU);
|
|
1307
|
+
X.fillStyle = 'rgba(255,252,245,' + (0.8 * intensity).toFixed(3) + ')'; X.fill();
|
|
1308
|
+
X.beginPath();
|
|
1309
|
+
X.moveTo(x1, y1);
|
|
1310
|
+
X.lineTo(ox + Math.cos(ang) * (r1 + 30), oy + Math.sin(ang) * (r1 + 30));
|
|
1311
|
+
X.strokeStyle = 'rgba(255,230,190,' + (0.25 * intensity).toFixed(3) + ')';
|
|
1312
|
+
X.lineWidth = 1; X.stroke();
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
if (CN.mode === 'beam' || CN.mode === 'both') {
|
|
1316
|
+
if (live.length) {
|
|
1317
|
+
/* 有工具在跑:射向它所在的那种工具的扇区(可能多条并发) */
|
|
1318
|
+
for (i = 0; i < live.length; i++) {
|
|
1319
|
+
var la2 = segMap[live[i].name];
|
|
1320
|
+
drawBeam(la2 == null ? -Math.PI / 2 : la2, R[3] + 2, live.length > 1 ? 0.78 : 1);
|
|
1321
|
+
}
|
|
1322
|
+
} else if (stpAll.length) {
|
|
1323
|
+
/* 空闲也画:指向最近那一步在 ② 环上的位置,别让这个模式看起来是空的 */
|
|
1324
|
+
var curS = stpAll[stpAll.length - 1];
|
|
1325
|
+
var angS = -Math.PI / 2 + ((tn - curS.t) / spanS2) * TAU + rotOf(1, t);
|
|
1326
|
+
drawBeam(angS, R[1] + 2, (M.turnOpen ? 0.55 : 0.34) * pulseK);
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
/* ---- 节点连线:跨圈的折线(核心 → 步骤 → 这一步用的工具类型 → 下一步 → …)
|
|
1331
|
+
工具环现在是"构成环",所以工具节点落在「它那种工具的扇区中心」上,不再是时间点。
|
|
1332
|
+
最近两轮都画;高亮的那一轮 = 有步骤的最新一轮(新一轮还没步骤时,上一轮继续亮) ---- */
|
|
1333
|
+
if (CN.mode === 'node' || CN.mode === 'both') {
|
|
1334
|
+
var stps = (M.sSteps || []);
|
|
1335
|
+
var hasCurStep = false;
|
|
1336
|
+
for (i = 0; i < stps.length; i++) if (stps[i].turn === M.turn) { hasCurStep = true; break; }
|
|
1337
|
+
var lastTurnWithSteps = stps.length ? stps[stps.length - 1].turn : -1;
|
|
1338
|
+
var activeTurn = M.turnOpen ? (hasCurStep ? M.turn : lastTurnWithSteps) : lastTurnWithSteps;
|
|
1339
|
+
|
|
1340
|
+
var turns = drawTurns.slice().reverse(); /* 旧 → 新,和 ② 环的"有没有线"用同一个集合 */
|
|
1341
|
+
|
|
1342
|
+
/* 审批 / 变更常发生在两个步骤之间(等审批时没有步骤在跑),
|
|
1343
|
+
按"步骤时间窗"永远接不上 —— 改成挂到"时间上最近的、在它之前的那个步骤" */
|
|
1344
|
+
var stepOf = function (tms) {
|
|
1345
|
+
var lo = 0, hi = stps.length - 1, ans = -1;
|
|
1346
|
+
while (lo <= hi) {
|
|
1347
|
+
var md = (lo + hi) >> 1;
|
|
1348
|
+
if (stps[md].t <= tms) { ans = md; lo = md + 1 } else hi = md - 1;
|
|
1349
|
+
}
|
|
1350
|
+
return ans;
|
|
1351
|
+
};
|
|
1352
|
+
var extras = {};
|
|
1353
|
+
var addExtra = function (list, ri) {
|
|
1354
|
+
if (!RINGVIS[ri]) return;
|
|
1355
|
+
(list || []).forEach(function (ev) {
|
|
1356
|
+
var si = stepOf(ev.t);
|
|
1357
|
+
if (si < 0) return;
|
|
1358
|
+
var sX = stps[si];
|
|
1359
|
+
var kX = sX.turn + '-' + sX.step;
|
|
1360
|
+
(extras[kX] = extras[kX] || []).push({ ri: ri, t: ev.t });
|
|
1361
|
+
});
|
|
1362
|
+
};
|
|
1363
|
+
addExtra(M.sAppr, 5);
|
|
1364
|
+
addExtra(M.sChg, 6);
|
|
1365
|
+
addExtra(M.sDeliv, 6);
|
|
1366
|
+
|
|
1367
|
+
var seg3 = function (p0, p1, bright, fr) {
|
|
1368
|
+
X.beginPath(); X.moveTo(p0.x, p0.y); X.lineTo(p1.x, p1.y);
|
|
1369
|
+
X.strokeStyle = bright
|
|
1370
|
+
? 'rgba(255,236,205,' + (0.16 + 0.5 * fr).toFixed(3) + ')'
|
|
1371
|
+
: 'rgba(168,184,210,0.30)';
|
|
1372
|
+
X.lineWidth = bright ? (1 + 1.4 * fr) : 1.1;
|
|
1373
|
+
X.stroke();
|
|
1374
|
+
X.beginPath(); X.arc(p1.x, p1.y, bright ? 1.8 : 1.1, 0, TAU);
|
|
1375
|
+
X.fillStyle = bright ? 'rgba(255,246,230,.8)' : 'rgba(168,184,210,.45)';
|
|
1376
|
+
X.fill();
|
|
1377
|
+
};
|
|
1378
|
+
|
|
1379
|
+
for (var ti3 = 0; ti3 < turns.length; ti3++) {
|
|
1380
|
+
var tno = turns[ti3];
|
|
1381
|
+
var arr3 = stps.filter(function (s) { return s.turn === tno; });
|
|
1382
|
+
if (arr3.length > 40) arr3 = arr3.slice(-40);
|
|
1383
|
+
if (!arr3.length) continue;
|
|
1384
|
+
var bright = (tno === activeTurn);
|
|
1385
|
+
for (i = 0; i < arr3.length; i++) {
|
|
1386
|
+
var s5 = arr3[i];
|
|
1387
|
+
var aS5;
|
|
1388
|
+
if (R2GEO && RINGVIS[1]) {
|
|
1389
|
+
/* ② 环是按时间片聚合的,落点必须对齐"这一片的中心",否则看起来错位 */
|
|
1390
|
+
var bi5 = Math.min(R2GEO.nb - 1, Math.max(0, Math.floor((s5.t - R2GEO.t0) / R2GEO.span * R2GEO.nb)));
|
|
1391
|
+
var bc5 = R2GEO.t0 + (bi5 + 0.5) / R2GEO.nb * R2GEO.span;
|
|
1392
|
+
aS5 = -Math.PI / 2 + ((tn - bc5) / R2GEO.span) * TAU + rotOf(1, t);
|
|
1393
|
+
} else {
|
|
1394
|
+
aS5 = -Math.PI / 2 + ((tn - s5.t) / spanS2) * TAU + rotOf(1, t);
|
|
1395
|
+
}
|
|
1396
|
+
var fr5 = (i + 1) / Math.max(1, arr3.length);
|
|
1397
|
+
/* 每一步一条独立的"阶梯":① → ② → ③ → ④,先把路径点收齐,再画线、再放光点 */
|
|
1398
|
+
var path = [
|
|
1399
|
+
{ x: ox + Math.cos(aS5) * R[0], y: oy + Math.sin(aS5) * R[0] },
|
|
1400
|
+
{ x: ox + Math.cos(aS5) * R[1], y: oy + Math.sin(aS5) * R[1] }
|
|
1401
|
+
];
|
|
1402
|
+
/* 这一步到底用没用技能:有 skill 调用才让折线绕一下 ③ 层,否则直接去 ④ 层 */
|
|
1403
|
+
var stepTools = (M.sTools || []).filter(function (x5) {
|
|
1404
|
+
return x5.turn === s5.turn && x5.step === s5.step;
|
|
1405
|
+
});
|
|
1406
|
+
var usedSkill = stepTools.some(function (x5) { return x5.name === 'skill'; });
|
|
1407
|
+
var used = 0;
|
|
1408
|
+
for (var tj = 0; tj < stepTools.length && used < 12; tj++) {
|
|
1409
|
+
var tl5 = stepTools[tj];
|
|
1410
|
+
var aT5 = segMap[tl5.name];
|
|
1411
|
+
if (aT5 == null) continue;
|
|
1412
|
+
if (usedSkill) {
|
|
1413
|
+
var aK = (SKILL_ANG == null) ? aT5 : SKILL_ANG;
|
|
1414
|
+
path.push({ x: ox + Math.cos(aK) * R[2], y: oy + Math.sin(aK) * R[2] });
|
|
1415
|
+
}
|
|
1416
|
+
path.push({ x: ox + Math.cos(aT5) * R[3], y: oy + Math.sin(aT5) * R[3] });
|
|
1417
|
+
used++;
|
|
1418
|
+
}
|
|
1419
|
+
/* ⑤⑥⑦:这一步的时间窗里如果发生了后台任务 / 审批 / 变更,折线继续往外接 */
|
|
1420
|
+
var w0 = s5.t, w1 = s5.end || (s5.t + 2500);
|
|
1421
|
+
/* 每层用自己画的时候用的那个时间尺度,否则落点会整体偏 */
|
|
1422
|
+
var angWin = function (tms, ri) {
|
|
1423
|
+
var span = RSPAN[ri] || spanS2;
|
|
1424
|
+
return -Math.PI / 2 + ((tn - tms) / span) * TAU + rotOf(ri, t);
|
|
1425
|
+
};
|
|
1426
|
+
/* ⑤ 任务:这一步时间窗里起来的后台任务 */
|
|
1427
|
+
var gotJ = 0;
|
|
1428
|
+
for (var m2 = 0; m2 < (M.sJobs || []).length && gotJ < 2; m2++) {
|
|
1429
|
+
var jt = M.sJobs[m2];
|
|
1430
|
+
if (jt.t < w0 - 800 || jt.t > w1 + 800) continue;
|
|
1431
|
+
var aJ3 = JOBARC[jt.id]; /* ⑤ 是按时长排弧的,只能用弧段中心 */
|
|
1432
|
+
if (aJ3 == null) continue;
|
|
1433
|
+
path.push({ x: ox + Math.cos(aJ3) * R[4], y: oy + Math.sin(aJ3) * R[4] });
|
|
1434
|
+
gotJ++;
|
|
1435
|
+
}
|
|
1436
|
+
/* ⑥⑦:按归属挂上来的审批 / 变更 */
|
|
1437
|
+
var exl = (extras[s5.turn + '-' + s5.step] || []).slice(0, 3);
|
|
1438
|
+
for (var xe = 0; xe < exl.length; xe++) {
|
|
1439
|
+
var e6 = exl[xe];
|
|
1440
|
+
var a6 = angWin(e6.t, e6.ri);
|
|
1441
|
+
path.push({ x: ox + Math.cos(a6) * R[e6.ri], y: oy + Math.sin(a6) * R[e6.ri] });
|
|
1442
|
+
}
|
|
1443
|
+
/* 这里只攒几何,真正的绘制在下一帧开头(画环之前)完成 —— 线在环下面 */
|
|
1444
|
+
if (path.length > 1) {
|
|
1445
|
+
CHAIN_PREV.push({
|
|
1446
|
+
pts: path, bright: bright, fr: fr5,
|
|
1447
|
+
phase: (i / Math.max(1, arr3.length)) * 0.8
|
|
1448
|
+
});
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
if (DEBUG) window.__CHAIN = CHAIN_PREV; /* 调试用:本帧攒下的连线几何 */
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
/* 层标签 */
|
|
1456
|
+
var lastStep = (M.sSteps && M.sSteps.length) ? M.sSteps[M.sSteps.length - 1] : null;
|
|
1457
|
+
var lastSec = lastStep ? Math.max(0, ((lastStep.end || tn) - lastStep.t) / 1000) : 0;
|
|
1458
|
+
var cnt = [
|
|
1459
|
+
Math.round((M.ctxPct || 0) * 100) + '%',
|
|
1460
|
+
lastSec.toFixed(1) + 's',
|
|
1461
|
+
M.skills.length + '',
|
|
1462
|
+
((M.sStats || []).filter(function (s) { return s.count > 0; }).length) + ' 种',
|
|
1463
|
+
'运行 ' + (M.sJobs || []).filter(function (j) { return j.state === 'RUN'; }).length + ' · 共 ' + (M.sJobs || []).length,
|
|
1464
|
+
(M.sAppr || []).length + ' 次',
|
|
1465
|
+
(M.sChg || []).length + ' 处'
|
|
1466
|
+
];
|
|
1467
|
+
var names = LANG === 'en'
|
|
1468
|
+
? ['① Session', '② Steps', '③ Skills', '④ Tools', '⑤ Jobs', '⑥ Approvals', '⑦ Changes']
|
|
1469
|
+
: ['① 会话', '② 步骤', '③ 技能', '④ 工具', '⑤ 任务', '⑥ 审批', '⑦ 变更'];
|
|
1470
|
+
var cols = RING_COL;
|
|
1471
|
+
X.font = '10px ui-monospace,Consolas,monospace';
|
|
1472
|
+
X.textAlign = 'center'; X.textBaseline = 'alphabetic';
|
|
1473
|
+
for (i = 0; i < RING_NAME.length; i++) {
|
|
1474
|
+
if (!RINGVIS[i]) continue;
|
|
1475
|
+
X.fillStyle = rgba(cols[i], 0.55);
|
|
1476
|
+
X.fillText(names[i], ox, oy - R[i] - 17);
|
|
1477
|
+
X.fillStyle = rgba(cols[i], 0.95);
|
|
1478
|
+
X.fillText(cnt[i], ox, oy - R[i] - 5);
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
/* ---------------- 循环 ---------------- */
|
|
1483
|
+
var last = 0;
|
|
1484
|
+
function frame(ts) {
|
|
1485
|
+
var dt = Math.min(0.06, (ts - last) / 1000); last = ts;
|
|
1486
|
+
if (lastToolCount !== M.toolCount) { pulse = 1; lastToolCount = M.toolCount; }
|
|
1487
|
+
pulse *= 0.92;
|
|
1488
|
+
try { draw(ts / 1000) } catch (e) { if (DEBUG) window.__DRAWERR = String((e && e.stack) || e) }
|
|
1489
|
+
requestAnimationFrame(frame);
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
resize();
|
|
1493
|
+
window.addEventListener('resize', function () { resize(); });
|
|
1494
|
+
setInterval(function () { $('tClock').textContent = hhmmss(RP.on ? clockNow() : undefined); }, 1000);
|
|
1495
|
+
(function () { var pb = $('tPop'); if (pb) pb.href = location.href; })();
|
|
1496
|
+
/* 新窗口:拖到另一个屏幕就是一块独立仪表 */
|
|
1497
|
+
var POP_KEY = 'dshviz.pop.size';
|
|
1498
|
+
function readPopSize() {
|
|
1499
|
+
try { return JSON.parse(localStorage.getItem(POP_KEY) || 'null') } catch (e) { return null }
|
|
1500
|
+
}
|
|
1501
|
+
/* 弹出来的那个窗口自己记尺寸(改完大小下次沿用) */
|
|
1502
|
+
if (window.name === 'dsh-viz-instrument') {
|
|
1503
|
+
(function () {
|
|
1504
|
+
var tm = 0;
|
|
1505
|
+
function save() {
|
|
1506
|
+
try {
|
|
1507
|
+
var w = window.outerWidth, hh = window.outerHeight;
|
|
1508
|
+
if (w > 400 && hh > 300) localStorage.setItem(POP_KEY, JSON.stringify({ w: w, h: hh }));
|
|
1509
|
+
} catch (e) { }
|
|
1510
|
+
}
|
|
1511
|
+
window.addEventListener('resize', function () { clearTimeout(tm); tm = setTimeout(save, 400) });
|
|
1512
|
+
window.addEventListener('beforeunload', save);
|
|
1513
|
+
})();
|
|
1514
|
+
}
|
|
1515
|
+
/* 桌面端页面跑在 dsh-app:// 协议下,那个地址粘到浏览器只会被当成搜索词。
|
|
1516
|
+
宿主随状态下发真实 http origin,用它拼一个浏览器能直接打开的地址。 */
|
|
1517
|
+
function browserUrl() {
|
|
1518
|
+
var o = M.origin || (M.bridge && M.bridge.origin) || '';
|
|
1519
|
+
if (!o && /^https?:$/.test(location.protocol)) o = location.origin;
|
|
1520
|
+
if (!o) return '';
|
|
1521
|
+
return o + location.pathname + location.search;
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
var popBtn = $('tPop');
|
|
1525
|
+
if (popBtn) {
|
|
1526
|
+
popBtn.href = browserUrl();
|
|
1527
|
+
|
|
1528
|
+
/* 桌面端(Electron)会把 window.open / target=_blank 全部吃掉,
|
|
1529
|
+
那种环境下直接给地址 + 一键复制,让人在浏览器里打开再拖到副屏。 */
|
|
1530
|
+
var isShell = /Electron/i.test(navigator.userAgent || '');
|
|
1531
|
+
function showUrlPanel() {
|
|
1532
|
+
var old = $('popPanel');
|
|
1533
|
+
if (old) { old.parentNode.removeChild(old); return }
|
|
1534
|
+
var box = document.createElement('div');
|
|
1535
|
+
box.id = 'popPanel';
|
|
1536
|
+
box.style.cssText = 'position:fixed;right:24px;top:56px;z-index:60;width:420px;padding:12px 14px;' +
|
|
1537
|
+
'background:rgba(10,12,16,.97);border:1px solid rgba(190,200,215,.32);border-radius:4px;' +
|
|
1538
|
+
'font:10px/1.6 ui-monospace,Consolas,monospace;color:var(--mid);box-shadow:0 10px 40px rgba(0,0,0,.6)';
|
|
1539
|
+
var url = browserUrl();
|
|
1540
|
+
var noOrigin = !url;
|
|
1541
|
+
if (!url) url = '(宿主还没上报 origin,刷新一次页面再试)';
|
|
1542
|
+
box.innerHTML =
|
|
1543
|
+
'<div style="color:var(--ink);font-size:11px;margin-bottom:6px">在独立窗口里看</div>' +
|
|
1544
|
+
'<div style="opacity:.7;margin-bottom:8px">桌面端不允许页面自己开窗。把下面地址粘到浏览器,' +
|
|
1545
|
+
'再把那个标签页拖到另一个屏幕。</div>' +
|
|
1546
|
+
'<div id="popUrl" style="user-select:all;word-break:break-all;padding:6px 8px;' +
|
|
1547
|
+
'background:rgba(255,255,255,.05);border:1px solid rgba(190,200,215,.18);margin-bottom:8px">' +
|
|
1548
|
+
esc(url) + '</div>' +
|
|
1549
|
+
'<b id="popCopy" style="cursor:pointer;border:1px solid rgba(190,200,215,.3);padding:2px 10px;' +
|
|
1550
|
+
'border-radius:3px;color:var(--ink)">复制地址</b>' +
|
|
1551
|
+
'<b id="popAgain" style="cursor:pointer;margin-left:8px;color:var(--dim)">再试一次弹窗</b>';
|
|
1552
|
+
document.body.appendChild(box);
|
|
1553
|
+
var copy = $('popCopy');
|
|
1554
|
+
if (copy) copy.addEventListener('click', function () {
|
|
1555
|
+
var text = url;
|
|
1556
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
1557
|
+
navigator.clipboard.writeText(text).then(function () { copy.textContent = '已复制' },
|
|
1558
|
+
function () { copy.textContent = '复制失败,请手动选中' });
|
|
1559
|
+
} else {
|
|
1560
|
+
var r = document.createRange();
|
|
1561
|
+
r.selectNodeContents($('popUrl'));
|
|
1562
|
+
var sel = window.getSelection();
|
|
1563
|
+
sel.removeAllRanges(); sel.addRange(r);
|
|
1564
|
+
copy.textContent = '已选中,Ctrl+C';
|
|
1565
|
+
}
|
|
1566
|
+
});
|
|
1567
|
+
var again = $('popAgain');
|
|
1568
|
+
if (again) again.addEventListener('click', function () { openPopup(true) });
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
function openPopup(e) {
|
|
1572
|
+
var saved = readPopSize();
|
|
1573
|
+
var w = Math.max(600, saved && saved.w ? saved.w : Math.round(window.screen.availWidth * 0.7));
|
|
1574
|
+
var hh = Math.max(400, saved && saved.h ? saved.h : Math.round(window.screen.availHeight * 0.8));
|
|
1575
|
+
var win = null;
|
|
1576
|
+
try {
|
|
1577
|
+
win = window.open(location.href, 'dsh-viz-instrument',
|
|
1578
|
+
'popup=yes,width=' + w + ',height=' + hh + ',menubar=no,toolbar=no,location=no,status=no');
|
|
1579
|
+
} catch (err) { win = null }
|
|
1580
|
+
if (win) {
|
|
1581
|
+
if (e) e.preventDefault();
|
|
1582
|
+
try { win.focus() } catch (err) { }
|
|
1583
|
+
return true;
|
|
1584
|
+
}
|
|
1585
|
+
return false;
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
popBtn.addEventListener('click', function (e) {
|
|
1589
|
+
if (isShell) { e.preventDefault(); showUrlPanel(); return }
|
|
1590
|
+
if (openPopup(e)) return;
|
|
1591
|
+
/* 浏览器里被拦:让 <a target="_blank"> 自己走;同时给个地址面板保底 */
|
|
1592
|
+
setTimeout(function () { if (!document.hidden) showUrlPanel() }, 900);
|
|
1593
|
+
});
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
var langBtn = $('langBtn');
|
|
1597
|
+
if (langBtn) {
|
|
1598
|
+
langBtn.textContent = LANG === 'zh' ? '中 / EN' : 'EN / 中';
|
|
1599
|
+
langBtn.addEventListener('click', function () {
|
|
1600
|
+
try { localStorage.setItem('dshviz.lang', LANG === 'zh' ? 'en' : 'zh') } catch (e) { }
|
|
1601
|
+
location.reload();
|
|
1602
|
+
});
|
|
1603
|
+
}
|
|
1604
|
+
applyPrefs();
|
|
1605
|
+
buildFilters();
|
|
1606
|
+
syncFilters();
|
|
1607
|
+
syncAllBtns();
|
|
1608
|
+
/* 地址栏参数:#ct=<连线轮数> #sc=<显示轮数>(0 = 全部),方便留一个固定视图 */
|
|
1609
|
+
(function () {
|
|
1610
|
+
var hp = {};
|
|
1611
|
+
String(location.hash).replace(/^#/, '').split('&').forEach(function (kv) {
|
|
1612
|
+
var p = kv.split('='); if (p[0]) hp[p[0]] = p[1];
|
|
1613
|
+
});
|
|
1614
|
+
if (hp.ct != null && !isNaN(+hp.ct)) CN.turns = +hp.ct;
|
|
1615
|
+
if (hp.sc != null && !isNaN(+hp.sc)) SCOPE.turns = +hp.sc;
|
|
1616
|
+
clampLineTurns();
|
|
1617
|
+
syncTurnBtns();
|
|
1618
|
+
})();
|
|
1619
|
+
loadSessions();
|
|
1620
|
+
var RPSTEPS = [300, 160, 90, 40], rpStepI = 2; /* 每条事件之间隔多少毫秒:越小越快 */
|
|
1621
|
+
function syncReplayUI() {
|
|
1622
|
+
var b = $('rpPlay'), s = $('rpSpeed'), l = $('rpLive'), pr = $('rpProg');
|
|
1623
|
+
if (!b) return;
|
|
1624
|
+
if (RP.on) {
|
|
1625
|
+
b.textContent = RP.playing ? '❚❚ 暂停' : (RP.done ? '↻ 重放' : '▶ 继续');
|
|
1626
|
+
s.style.display = 'inline-block';
|
|
1627
|
+
s.textContent = RP.stepMs + 'ms/条';
|
|
1628
|
+
l.style.display = 'inline-block';
|
|
1629
|
+
pr.textContent = RP.mode === 'seq'
|
|
1630
|
+
? '事件 ' + (RP.idx + 1) + ' / ' + RP.total + ' · ' + hhmmss(RP.vtime)
|
|
1631
|
+
: Math.round(RP.pct * 100) + '% · ' + hhmmss(RP.vtime);
|
|
1632
|
+
} else {
|
|
1633
|
+
b.textContent = '▶ 重播';
|
|
1634
|
+
s.textContent = RPSTEPS[rpStepI] + 'ms/条';
|
|
1635
|
+
l.style.display = 'none';
|
|
1636
|
+
pr.textContent = '';
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
function replayCall(qs) {
|
|
1640
|
+
return fetch('replay?' + qs, { cache: 'no-store' })
|
|
1641
|
+
.then(function (r) { return r.json() })
|
|
1642
|
+
.then(function () { return pull() })
|
|
1643
|
+
.catch(function () { });
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
var cnBtn = $('cnBtn');
|
|
1647
|
+
if (cnBtn) cnBtn.addEventListener('click', function () {
|
|
1648
|
+
var k = CN.list.indexOf(CN.mode);
|
|
1649
|
+
CN.mode = CN.list[(k + 1) % CN.list.length];
|
|
1650
|
+
cnBtn.textContent = '连线:' + CN.label[CN.mode];
|
|
1651
|
+
savePrefs();
|
|
1652
|
+
});
|
|
1653
|
+
var shBtn = $('shBtn');
|
|
1654
|
+
if (shBtn) shBtn.addEventListener('click', function () {
|
|
1655
|
+
CN.share = (CN.share === 'count') ? 'ms' : 'count';
|
|
1656
|
+
shBtn.textContent = '占比:' + (CN.share === 'count' ? '次数' : '耗时');
|
|
1657
|
+
savePrefs();
|
|
1658
|
+
});
|
|
1659
|
+
/* 约束:连线轮数 ≤ 显示轮数(显示收窄到 N 轮时,连线不该超过 N 轮) */
|
|
1660
|
+
function lineOpts() {
|
|
1661
|
+
if (!SCOPE.turns) return CN.turnOpts;
|
|
1662
|
+
var out = CN.turnOpts.filter(function (v) { return v && v <= SCOPE.turns });
|
|
1663
|
+
return out.length ? out : [SCOPE.turns];
|
|
1664
|
+
}
|
|
1665
|
+
function syncTurnBtns() {
|
|
1666
|
+
var tn = $('tnBtn'), sc = $('scBtn');
|
|
1667
|
+
if (tn) {
|
|
1668
|
+
var tot = M.turn || 0;
|
|
1669
|
+
tn.textContent = '连线轮数:' + (CN.turns || '全部') + (tot ? ' / 共 ' + tot + ' 轮' : '');
|
|
1670
|
+
}
|
|
1671
|
+
if (sc) sc.textContent = '显示轮数:' + (SCOPE.turns || '全部');
|
|
1672
|
+
}
|
|
1673
|
+
function clampLineTurns() {
|
|
1674
|
+
if (!SCOPE.turns) return;
|
|
1675
|
+
if (!CN.turns || CN.turns > SCOPE.turns) CN.turns = SCOPE.turns;
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
var tnBtn = $('tnBtn');
|
|
1679
|
+
if (tnBtn) tnBtn.addEventListener('click', function () {
|
|
1680
|
+
var opts = lineOpts();
|
|
1681
|
+
var k = opts.indexOf(CN.turns);
|
|
1682
|
+
CN.turns = opts[(k + 1) % opts.length];
|
|
1683
|
+
syncTurnBtns();
|
|
1684
|
+
savePrefs();
|
|
1685
|
+
});
|
|
1686
|
+
var scBtn = $('scBtn');
|
|
1687
|
+
if (scBtn) scBtn.addEventListener('click', function () {
|
|
1688
|
+
var k = SCOPE.opts.indexOf(SCOPE.turns);
|
|
1689
|
+
SCOPE.turns = SCOPE.opts[(k + 1) % SCOPE.opts.length];
|
|
1690
|
+
clampLineTurns();
|
|
1691
|
+
syncTurnBtns();
|
|
1692
|
+
savePrefs();
|
|
1693
|
+
render();
|
|
1694
|
+
});
|
|
1695
|
+
/* 环显隐开关:多选,点一下切换一个 */
|
|
1696
|
+
(function () {
|
|
1697
|
+
var box = $('ringToggles');
|
|
1698
|
+
if (!box) return;
|
|
1699
|
+
box.innerHTML = '<b class="lb" style="margin-right:7px">环</b>' + RING_NAME.map(function (nm, i) {
|
|
1700
|
+
var c = RING_COL[i];
|
|
1701
|
+
return '<b class="chip' + (RINGVIS[i] ? ' on' : '') + '" data-ring="' + i +
|
|
1702
|
+
'" style="color:' + c + ';border-color:' + (RINGVIS[i] ? c : 'rgba(190,200,215,.22)') + '">' + nm + '</b>';
|
|
1703
|
+
}).join('');
|
|
1704
|
+
box.addEventListener('click', function (e) {
|
|
1705
|
+
var k = e.target && e.target.getAttribute && e.target.getAttribute('data-ring');
|
|
1706
|
+
if (k == null) return;
|
|
1707
|
+
var idx = +k;
|
|
1708
|
+
RINGVIS[idx] = !RINGVIS[idx];
|
|
1709
|
+
var c = RING_COL[idx];
|
|
1710
|
+
e.target.className = 'chip' + (RINGVIS[idx] ? ' on' : '');
|
|
1711
|
+
e.target.style.borderColor = RINGVIS[idx] ? c : 'rgba(190,200,215,.22)';
|
|
1712
|
+
savePrefs();
|
|
1713
|
+
});
|
|
1714
|
+
})();
|
|
1715
|
+
|
|
1716
|
+
var rotBtn = $('rotBtn');
|
|
1717
|
+
if (rotBtn) rotBtn.addEventListener('click', function () {
|
|
1718
|
+
ROT.on = !ROT.on;
|
|
1719
|
+
rotBtn.textContent = '环旋转:' + (ROT.on ? '开' : '关');
|
|
1720
|
+
rotBtn.style.color = ROT.on ? 'var(--mid)' : 'var(--dim)';
|
|
1721
|
+
savePrefs();
|
|
1722
|
+
});
|
|
1723
|
+
/* ---------------- 任意环上的节点:悬停看名字,点击看详情 ---------------- */
|
|
1724
|
+
function hitAt(mx, my) {
|
|
1725
|
+
if (!HIT.ox) return null;
|
|
1726
|
+
var dx = mx - HIT.ox, dy = my - HIT.oy;
|
|
1727
|
+
var r = Math.sqrt(dx * dx + dy * dy);
|
|
1728
|
+
var ang = Math.atan2(dy, dx);
|
|
1729
|
+
var best = null, bestD = 1e9;
|
|
1730
|
+
for (var k = 0; k < HIT.items.length; k++) {
|
|
1731
|
+
var it = HIT.items[k];
|
|
1732
|
+
var d = Math.abs(r - it.r);
|
|
1733
|
+
if (d > 12) continue;
|
|
1734
|
+
var rel = ((ang - it.a0) % TAU + TAU) % TAU;
|
|
1735
|
+
if (rel > (it.a1 - it.a0)) continue;
|
|
1736
|
+
if (d < bestD) { bestD = d; best = it; }
|
|
1737
|
+
}
|
|
1738
|
+
return best;
|
|
1739
|
+
}
|
|
1740
|
+
function fmtMs(v) { return v >= 1000 ? (v / 1000).toFixed(1) + 's' : Math.round(v) + 'ms'; }
|
|
1741
|
+
var tipEl = $('tip'), detEl = $('tdetail');
|
|
1742
|
+
function showTip(it, mx, my) {
|
|
1743
|
+
if (!tipEl) return;
|
|
1744
|
+
tipEl.innerHTML = esc(it.label) + (it.sub ? '<em>' + esc(it.sub) + '</em>' : '');
|
|
1745
|
+
tipEl.style.left = (mx + 14) + 'px';
|
|
1746
|
+
tipEl.style.top = (my + 14) + 'px';
|
|
1747
|
+
tipEl.classList.add('on');
|
|
1748
|
+
}
|
|
1749
|
+
function hideTip() { if (tipEl) tipEl.classList.remove('on'); }
|
|
1750
|
+
|
|
1751
|
+
function drow(k, v, bad) {
|
|
1752
|
+
return '<div class="row"><span>' + esc(k) + '</span><b' + (bad ? ' style="color:var(--bad)"' : '') + '>' + v + '</b></div>';
|
|
1753
|
+
}
|
|
1754
|
+
function showDetail(it, mx, my) {
|
|
1755
|
+
if (!detEl) return;
|
|
1756
|
+
var h = '', list = '', listTitle = '';
|
|
1757
|
+
if (it.kind === 'tool') {
|
|
1758
|
+
var st = (M.sStats || []).filter(function (s) { return s.name === it.data.name; })[0];
|
|
1759
|
+
if (!st) return;
|
|
1760
|
+
h = '<h5>' + esc(cn(TOOL_CN, it.data.name) || it.data.name) + '<em>' + esc(it.data.name) + '</em></h5>' +
|
|
1761
|
+
drow('调用次数', st.count) +
|
|
1762
|
+
drow('累计 / 平均', fmtMs(st.ms) + ' / ' + fmtMs(st.count ? st.ms / st.count : 0)) +
|
|
1763
|
+
drow('失败 / 最近', st.fail + ' / ' + ago(st.last) + '前', st.fail > 0);
|
|
1764
|
+
listTitle = '最近几次调用';
|
|
1765
|
+
fetchCalls(it.data.name);
|
|
1766
|
+
} else if (it.kind === 'turn') {
|
|
1767
|
+
var tr = (M.turnsList || []).filter(function (x) { return x.no === it.data.no })[0];
|
|
1768
|
+
h = '<h5>轮次 ' + it.data.no + '<em>TURN</em></h5>' +
|
|
1769
|
+
drow('你的请求', tr ? esc(tr.prompt || '—') : '—') +
|
|
1770
|
+
drow('步数 / 工具', tr ? (tr.steps + ' / ' + tr.tools) : '—') +
|
|
1771
|
+
drow('时长', tr ? fmtMs((tr.end || clockNow()) - tr.start) : '—') +
|
|
1772
|
+
drow('交付', tr ? (tr.files + ' 个文件') : '—');
|
|
1773
|
+
} else if (it.kind === 'step') {
|
|
1774
|
+
var steps = (M.sSteps || []).filter(function (s) { return s.t >= it.data.t0 && s.t < it.data.t1 });
|
|
1775
|
+
var tot = 0;
|
|
1776
|
+
steps.forEach(function (s) { tot += Math.max(0, (s.end || clockNow()) - s.t) });
|
|
1777
|
+
var tools = (M.sTools || []).filter(function (x) { return x.t >= it.data.t0 && x.t < it.data.t1 });
|
|
1778
|
+
h = '<h5>步骤段<em>' + steps.length + ' 步</em></h5>' +
|
|
1779
|
+
drow('时间窗', hhmmss(it.data.t0) + ' ~ ' + hhmmss(it.data.t1)) +
|
|
1780
|
+
drow('累计耗时', fmtMs(tot)) +
|
|
1781
|
+
drow('工具调用', tools.length) +
|
|
1782
|
+
drow('涉及轮次', (steps[0] ? 'T' + steps[0].turn : '—'));
|
|
1783
|
+
listTitle = '这几步';
|
|
1784
|
+
list = steps.slice(-6).reverse().map(function (s) {
|
|
1785
|
+
return '<div class="cmd"><span class="t">' + hhmmss(s.t) + '</span><span>T' + s.turn + ' · 第 ' + s.step +
|
|
1786
|
+
' 步 · ' + fmtMs(Math.max(0, (s.end || clockNow()) - s.t)) + '</span></div>';
|
|
1787
|
+
}).join('');
|
|
1788
|
+
} else if (it.kind === 'skill') {
|
|
1789
|
+
var sk = (M.skills || [])[it.data.i] || {};
|
|
1790
|
+
h = '<h5>' + esc(cn(SKILL_CN, sk.name) || sk.name || '技能') + '<em>' + esc(sk.name || '') + '</em></h5>' +
|
|
1791
|
+
drow('加载次数', sk.uses || 0) +
|
|
1792
|
+
drow('最近使用', sk.last ? ago(sk.last) + '前' : '—');
|
|
1793
|
+
} else if (it.kind === 'job') {
|
|
1794
|
+
var jb = (M.sJobs || []).filter(function (x2) { return x2.id === it.data.id })[0] || {};
|
|
1795
|
+
h = '<h5>' + esc(cn(JOB_CN, jb.kind) || '后台任务') + '<em>' + esc(jb.state || '') + '</em></h5>' +
|
|
1796
|
+
drow('运行时长', fmtMs((jb.end || clockNow()) - (jb.t || clockNow()))) +
|
|
1797
|
+
drow('状态', esc(jb.state || '—'));
|
|
1798
|
+
listTitle = '命令';
|
|
1799
|
+
list = '<div class="cmd"><span>' + esc(String(jb.label || '').slice(0, 200)) + '</span></div>';
|
|
1800
|
+
} else if (it.kind === 'approval') {
|
|
1801
|
+
var ap = (M.approvalsList || [])[it.data.i] || {};
|
|
1802
|
+
h = '<h5>审批<em>' + esc(ap.tool || 'tool') + '</em></h5>' +
|
|
1803
|
+
drow('时间', hhmmss(ap.t)) +
|
|
1804
|
+
drow('结果', esc(ap.outcome || '待定'), /deny|reject/i.test(ap.outcome || ''));
|
|
1805
|
+
} else if (it.kind === 'change' || it.kind === 'deliv') {
|
|
1806
|
+
var ch = it.data || {};
|
|
1807
|
+
h = '<h5>' + (it.kind === 'deliv' ? '交付文件' : '文件变更') + '<em>T' + (ch.turn || '?') + '</em></h5>' +
|
|
1808
|
+
drow('时间', hhmmss(ch.t)) +
|
|
1809
|
+
drow('轮次', 'T' + (ch.turn || '?'));
|
|
1810
|
+
}
|
|
1811
|
+
detEl.innerHTML = h + (listTitle ? '<div class="sub" id="dsub">' + listTitle + '</div>' +
|
|
1812
|
+
'<div id="dcalls">' + (list || '<div class="cmd" style="color:var(--dim)">载入中…</div>') + '</div>' : '');
|
|
1813
|
+
detEl.classList.add('on');
|
|
1814
|
+
detEl.style.left = Math.max(12, Math.min(window.innerWidth - 346, mx + 18)) + 'px';
|
|
1815
|
+
detEl.style.top = Math.max(12, Math.min(window.innerHeight - 330, my - 20)) + 'px';
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1818
|
+
/* 工具的参数不进 /state(会把轮询放大),点开时按需拉 */
|
|
1819
|
+
function fetchCalls(name) {
|
|
1820
|
+
fetch('calls?tool=' + encodeURIComponent(name) + '&limit=6', { cache: 'no-store' })
|
|
1821
|
+
.then(function (r) { return r.json() })
|
|
1822
|
+
.then(function (j) {
|
|
1823
|
+
var box = $('dcalls'), sub = $('dsub');
|
|
1824
|
+
if (!box) return;
|
|
1825
|
+
var cs = j.calls || [];
|
|
1826
|
+
if (sub) sub.textContent = '最近 ' + cs.length + ' 次调用';
|
|
1827
|
+
box.innerHTML = cs.map(function (c) {
|
|
1828
|
+
var txt = String(c.args || '');
|
|
1829
|
+
if (txt.length > 150) txt = txt.slice(0, 149) + '…';
|
|
1830
|
+
return '<div class="cmd"><span class="t">' + hhmmss(c.t) + '</span>' +
|
|
1831
|
+
'<span title="' + esc(c.args) + '">' + esc(txt) + '</span></div>';
|
|
1832
|
+
}).join('') || '<div class="cmd" style="color:var(--dim)">—</div>';
|
|
1833
|
+
})
|
|
1834
|
+
.catch(function () {
|
|
1835
|
+
var box = $('dcalls');
|
|
1836
|
+
if (box) box.innerHTML = '<div class="cmd" style="color:var(--dim)">读不到调用记录</div>';
|
|
1837
|
+
});
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1840
|
+
if (net) {
|
|
1841
|
+
net.addEventListener('mousemove', function (e) {
|
|
1842
|
+
var it = hitAt(e.clientX, e.clientY);
|
|
1843
|
+
if (it) { showTip(it, e.clientX, e.clientY); net.style.cursor = 'pointer'; }
|
|
1844
|
+
else { hideTip(); net.style.cursor = 'default'; }
|
|
1845
|
+
});
|
|
1846
|
+
net.addEventListener('mouseleave', hideTip);
|
|
1847
|
+
net.addEventListener('click', function (e) {
|
|
1848
|
+
var it2 = hitAt(e.clientX, e.clientY);
|
|
1849
|
+
if (it2) showDetail(it2, e.clientX, e.clientY);
|
|
1850
|
+
else if (detEl) detEl.classList.remove('on');
|
|
1851
|
+
});
|
|
1852
|
+
document.addEventListener('keydown', function (e) {
|
|
1853
|
+
if (e.key === 'Escape' && detEl) detEl.classList.remove('on');
|
|
1854
|
+
});
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1857
|
+
var rpPlay = $('rpPlay');
|
|
1858
|
+
if (rpPlay) rpPlay.addEventListener('click', function () {
|
|
1859
|
+
if (!RP.on) replayCall('action=start&mode=seq&step=' + RPSTEPS[rpStepI]);
|
|
1860
|
+
else if (RP.playing) replayCall('action=pause');
|
|
1861
|
+
else if (RP.done) replayCall('action=restart&mode=seq&step=' + RPSTEPS[rpStepI]);
|
|
1862
|
+
else replayCall('action=resume');
|
|
1863
|
+
});
|
|
1864
|
+
var rpSpd = $('rpSpeed');
|
|
1865
|
+
if (rpSpd) rpSpd.addEventListener('click', function () {
|
|
1866
|
+
rpStepI = (rpStepI + 1) % RPSTEPS.length;
|
|
1867
|
+
rpSpd.textContent = RPSTEPS[rpStepI] + 'ms/条';
|
|
1868
|
+
if (RP.on) replayCall('action=resume&step=' + RPSTEPS[rpStepI]);
|
|
1869
|
+
});
|
|
1870
|
+
var rpLive = $('rpLive');
|
|
1871
|
+
if (rpLive) rpLive.addEventListener('click', function () { replayCall('action=stop'); });
|
|
1872
|
+
|
|
1873
|
+
var pickEl = $('pick');
|
|
1874
|
+
if (pickEl) pickEl.addEventListener('change', function (e) {
|
|
1875
|
+
fetch('select?session=' + encodeURIComponent(e.target.value || ''), { cache: 'no-store' })
|
|
1876
|
+
.then(function () { return loadSessions() })
|
|
1877
|
+
.then(function () { pull() })
|
|
1878
|
+
.catch(function () { });
|
|
1879
|
+
});
|
|
1880
|
+
pull();
|
|
1881
|
+
startStream();
|
|
1882
|
+
requestAnimationFrame(function (ts) { last = ts; requestAnimationFrame(frame); });
|
|
1883
|
+
})();
|
|
1884
|
+
</script>
|
|
1885
|
+
|
|
1886
|
+
|
|
1887
|
+
</body></html>
|