codebee 0.1.95 → 0.1.97
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/CHANGELOG.md +733 -720
- package/LICENSE +21 -21
- package/README.md +516 -509
- package/app/core/acceptance.py +100 -100
- package/app/core/agent_context.py +118 -0
- package/app/core/aiflavor.py +197 -197
- package/app/core/analytics.py +38 -38
- package/app/core/artifacts.py +148 -148
- package/app/core/assets.py +110 -110
- package/app/core/attachments.py +623 -623
- package/app/core/audit.py +30 -30
- package/app/core/backup.py +470 -470
- package/app/core/beekeeper.py +183 -183
- package/app/core/benchstore.py +102 -95
- package/app/core/board.py +246 -209
- package/app/core/bookmeta.py +1036 -1036
- package/app/core/bookmeta_catalog.py +180 -180
- package/app/core/branching.py +137 -137
- package/app/core/builtin_agent.py +2055 -1680
- package/app/core/capability.py +144 -144
- package/app/core/chaptersafe.py +32 -32
- package/app/core/checkpoints.py +648 -0
- package/app/core/cleanup.py +303 -303
- package/app/core/compaction.py +201 -201
- package/app/core/contracts.py +255 -255
- package/app/core/covergen.py +356 -356
- package/app/core/defectretro.py +43 -43
- package/app/core/diagnostics.py +114 -114
- package/app/core/dispatch.py +198 -191
- package/app/core/dispatch_log.py +128 -128
- package/app/core/env_scrub.py +85 -85
- package/app/core/errorlog.py +137 -137
- package/app/core/eval_matrix.py +82 -0
- package/app/core/evalbench.py +184 -91
- package/app/core/evaluation.py +174 -174
- package/app/core/failure_notify.py +290 -290
- package/app/core/flow_graph.py +33 -0
- package/app/core/goal_service.py +167 -167
- package/app/core/health.py +389 -389
- package/app/core/history.py +38 -38
- package/app/core/hooks.py +162 -162
- package/app/core/jobs.py +25 -4
- package/app/core/knowledge_pipeline.py +86 -0
- package/app/core/manager.py +2232 -2232
- package/app/core/mcp_client.py +200 -29
- package/app/core/mocks.py +64 -64
- package/app/core/notify.py +201 -201
- package/app/core/novel_quality.py +181 -189
- package/app/core/openapi.py +177 -154
- package/app/core/operations.py +228 -228
- package/app/core/paihang.py +145 -145
- package/app/core/pipeline.py +319 -59
- package/app/core/planner.py +17 -11
- package/app/core/policy.py +84 -0
- package/app/core/portguard.py +111 -111
- package/app/core/presence.py +77 -77
- package/app/core/preview.py +293 -293
- package/app/core/project_memory.py +139 -0
- package/app/core/prompt_guard.py +78 -78
- package/app/core/publish/__init__.py +7 -7
- package/app/core/publish/auto.py +106 -22
- package/app/core/publish/browser.py +694 -655
- package/app/core/publish/chaprows.py +138 -0
- package/app/core/publish/fanqie.py +212 -165
- package/app/core/publish/flow.py +832 -640
- package/app/core/publish/flows-fanqie-calibrated.json +361 -336
- package/app/core/publish/ledger.py +51 -2
- package/app/core/publish/manager.py +1150 -1118
- package/app/core/publish/qimao.py +176 -176
- package/app/core/publish/volumes.py +108 -0
- package/app/core/publish/ws.py +139 -139
- package/app/core/quality_gate.py +194 -194
- package/app/core/redact.py +38 -38
- package/app/core/repeat_guard.py +129 -129
- package/app/core/retrieval.py +137 -137
- package/app/core/router.py +398 -398
- package/app/core/runner.py +2239 -2238
- package/app/core/selfupdate.py +384 -384
- package/app/core/session_log.py +161 -161
- package/app/core/sessions.py +312 -312
- package/app/core/settings.py +33 -5
- package/app/core/settings_schema.py +282 -271
- package/app/core/share_page.py +114 -114
- package/app/core/skill_scan.py +139 -139
- package/app/core/skillpacks/fanqie-novel.md +8 -6
- package/app/core/skillpacks/market/character-bible.md +66 -66
- package/app/core/skillpacks/market/code-risk-checklist.md +58 -58
- package/app/core/skillpacks/market/git-workflow.md +57 -57
- package/app/core/skillpacks/market/release-notes.md +72 -72
- package/app/core/skillpacks/market/weekly-report.md +71 -71
- package/app/core/skillpacks/market/worldview-consistency.md +70 -70
- package/app/core/skillpacks/qimao-signing.md +14 -11
- package/app/core/skills.py +32 -21
- package/app/core/step_runner.py +132 -132
- package/app/core/store.py +24 -0
- package/app/core/story_tracking.py +275 -275
- package/app/core/task_compile.py +199 -199
- package/app/core/telemetry.py +292 -292
- package/app/core/tlsctx.py +64 -64
- package/app/core/token_meter.py +147 -147
- package/app/core/tool_outputs.py +107 -0
- package/app/core/tracing.py +207 -144
- package/app/core/upstream.py +41 -41
- package/app/core/usage.py +1038 -1038
- package/app/core/volumes.py +389 -389
- package/app/core/webhooks.py +89 -0
- package/app/core/wxdigest.py +865 -858
- package/app/core/zentao.py +2692 -2692
- package/app/main.py +179 -10
- package/app/mcp_server.py +244 -244
- package/app/pick_dialog.py +110 -110
- package/app/ui/app.js +563 -21
- package/app/ui/artdiff.js +74 -74
- package/app/ui/assets/community-qr.png +0 -0
- package/app/ui/assets/donate-alipay.jpg +0 -0
- package/app/ui/assets/donate-qr.jpg +0 -0
- package/app/ui/assets/workbench-reference.png +0 -0
- package/app/ui/board.css +451 -666
- package/app/ui/board.html +165 -162
- package/app/ui/board.js +509 -305
- package/app/ui/hive3d.js +242 -0
- package/app/ui/i18n.js +125 -9
- package/app/ui/icons/bee.svg +79 -79
- package/app/ui/index.html +60 -3
- package/app/ui/manifest.json +16 -16
- package/app/ui/qrcode.js +2297 -2297
- package/app/ui/style.css +189 -9
- package/bin/tutti.js +147 -147
- package/package.json +49 -48
- package/tools/wx_reader.py +261 -261
- package/app/.playwright-cli/console-2026-09-23T08-26-03-635Z.log +0 -88
- package/app/.playwright-cli/page-2026-09-23T08-26-03-845Z.yml +0 -126
- package/app/.playwright-cli/page-2026-09-23T08-29-44-203Z.png +0 -0
- package/app/.playwright-cli/page-2026-09-23T08-35-42-046Z.yml +0 -210
- package/app/.playwright-cli/page-2026-09-23T08-36-52-858Z.png +0 -0
- package/app/.playwright-cli/page-2026-09-23T08-37-33-938Z.png +0 -0
- package/app/.playwright-cli/run-detail-after.png +0 -0
- package/app/output/_createbook_body.json +0 -1
- package/app/output/_probe_fq_click.py +0 -50
- package/app/output/_probe_fq_form.py +0 -42
- package/app/output/_probe_fq_modal.py +0 -55
- package/app/output/_probe_fq_points.py +0 -71
- package/app/output/_probe_fq_submit.py +0 -34
- package/app/output/_probe_fq_watch.py +0 -62
package/app/ui/board.js
CHANGED
|
@@ -1,305 +1,509 @@
|
|
|
1
|
-
/* CodeBee 任务驾驶舱(/board.html):4s 轮询 /api/board(KB 级聚合端点),
|
|
2
|
-
不碰 /api/state 全量,也不占 SSE
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
const
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
//
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
if (n >=
|
|
24
|
-
if (n >=
|
|
25
|
-
return
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
if (n >=
|
|
31
|
-
return "$" + n.toFixed(
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
function
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
$("#gate-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
$("#
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
//
|
|
143
|
-
const
|
|
144
|
-
const
|
|
145
|
-
const
|
|
146
|
-
$("#
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
$("#
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
const
|
|
164
|
-
$("#
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
1
|
+
/* CodeBee 任务驾驶舱(/board.html):4s 轮询 /api/board(KB 级聚合端点),
|
|
2
|
+
不碰 /api/state 全量,也不占 SSE。下钻抽屉按需拉 /api/board/run 与
|
|
3
|
+
/api/runs/<id>/log,不参与 4s 轮询。 */
|
|
4
|
+
"use strict";
|
|
5
|
+
|
|
6
|
+
const $ = (s) => document.querySelector(s);
|
|
7
|
+
const t = (...a) => (window.t ? window.t(...a) : a[0]); // i18n.js 先于本脚本加载;透传占位符参数({0}/{1})
|
|
8
|
+
const POLL_MS = 4000;
|
|
9
|
+
|
|
10
|
+
// URL ?lang=en|zh 可覆盖语言(i18n.js 已按 localStorage 自动 apply 过一次,
|
|
11
|
+
// 这里写回偏好并重刷静态文案;动态文案走 t() 每次渲染实时取)
|
|
12
|
+
try {
|
|
13
|
+
const ql = new URLSearchParams(location.search).get("lang");
|
|
14
|
+
if ((ql === "en" || ql === "zh") && window.getLang && window.getLang() !== ql) {
|
|
15
|
+
window.setLang(ql);
|
|
16
|
+
if (window.applyI18n) window.applyI18n();
|
|
17
|
+
}
|
|
18
|
+
} catch (e) { /* ignore */ }
|
|
19
|
+
|
|
20
|
+
/* ---------------------------------------------------------------- 工具 */
|
|
21
|
+
function fmtTokens(n) {
|
|
22
|
+
n = Number(n) || 0;
|
|
23
|
+
if (n >= 1e8) return (n / 1e8).toFixed(2) + t("亿");
|
|
24
|
+
if (n >= 1e4) return (n / 1e4).toFixed(1) + t("万");
|
|
25
|
+
if (n >= 1e3) return (n / 1e3).toFixed(1) + "k";
|
|
26
|
+
return String(n);
|
|
27
|
+
}
|
|
28
|
+
function fmtCost(n) {
|
|
29
|
+
n = Number(n) || 0;
|
|
30
|
+
if (n >= 100) return "$" + n.toFixed(0);
|
|
31
|
+
if (n >= 1) return "$" + n.toFixed(2);
|
|
32
|
+
return "$" + n.toFixed(3);
|
|
33
|
+
}
|
|
34
|
+
function fmtDur(sec) {
|
|
35
|
+
sec = Math.max(0, Math.floor(Number(sec) || 0));
|
|
36
|
+
const h = Math.floor(sec / 3600), m = Math.floor((sec % 3600) / 60), s = sec % 60;
|
|
37
|
+
if (h) return h + "h" + String(m).padStart(2, "0") + "m";
|
|
38
|
+
return String(m).padStart(2, "0") + ":" + String(s).padStart(2, "0");
|
|
39
|
+
}
|
|
40
|
+
function parseTs(s) {
|
|
41
|
+
// 后端时间格式 "YYYY-MM-DD HH:MM:SS";补 T 让各浏览器 Date 都能解析
|
|
42
|
+
const t = Date.parse(String(s || "").replace(" ", "T"));
|
|
43
|
+
return isNaN(t) ? 0 : t;
|
|
44
|
+
}
|
|
45
|
+
function esc(s) {
|
|
46
|
+
return String(s == null ? "" : s).replace(/[&<>"']/g,
|
|
47
|
+
(c) => ({"&": "&", "<": "<", ">": ">", '"': """, "'": "'"}[c]));
|
|
48
|
+
}
|
|
49
|
+
function hmms(s) { return String(s || "").slice(11, 16); }
|
|
50
|
+
|
|
51
|
+
/* ---------------------------------------------------------------- 令牌门 */
|
|
52
|
+
function token() { return localStorage.getItem("orch.token") || ""; }
|
|
53
|
+
|
|
54
|
+
function showGate(msg) {
|
|
55
|
+
$("#gate").classList.remove("hidden");
|
|
56
|
+
$("#gate-err").textContent = msg || "";
|
|
57
|
+
setTimeout(() => $("#gate-token").focus(), 50);
|
|
58
|
+
}
|
|
59
|
+
$("#gate-ok").addEventListener("click", submitGate);
|
|
60
|
+
$("#gate-token").addEventListener("keydown", (e) => { if (e.key === "Enter") submitGate(); });
|
|
61
|
+
function submitGate() {
|
|
62
|
+
const v = $("#gate-token").value.trim();
|
|
63
|
+
if (!v) return;
|
|
64
|
+
localStorage.setItem("orch.token", v);
|
|
65
|
+
$("#gate").classList.add("hidden");
|
|
66
|
+
poll();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/* ---------------------------------------------------------------- 换肤
|
|
70
|
+
大屏独立记忆:orch.boardSkin / orch.boardTheme 优先,缺省时继承主界面的
|
|
71
|
+
orch.skin / orch.theme(第一次打开跟主界面一致),再缺省落 ocean/dark。
|
|
72
|
+
只写 html 的 data-skin/data-theme,值集与主应用 style.css 同一套。 */
|
|
73
|
+
const SKINS = [["ocean", "深海"], ["classic", "经典"], ["forest", "森林"],
|
|
74
|
+
["amber", "暖阳"], ["violet", "霓虹"], ["contrast", "高对比"]];
|
|
75
|
+
const SKIN_IDS = new Set(SKINS.map((s) => s[0]));
|
|
76
|
+
const _q = new URLSearchParams(location.search);
|
|
77
|
+
|
|
78
|
+
function skin() {
|
|
79
|
+
const v = _q.get("skin") || localStorage.getItem("orch.boardSkin") || localStorage.getItem("orch.skin") || "ocean";
|
|
80
|
+
return SKIN_IDS.has(v) ? v : "ocean";
|
|
81
|
+
}
|
|
82
|
+
function mode() {
|
|
83
|
+
const v = _q.get("mode") || localStorage.getItem("orch.boardTheme") || localStorage.getItem("orch.theme") || "dark";
|
|
84
|
+
return v === "light" ? "light" : "dark";
|
|
85
|
+
}
|
|
86
|
+
function applySkin() {
|
|
87
|
+
const r = document.documentElement;
|
|
88
|
+
r.dataset.skin = skin();
|
|
89
|
+
r.dataset.theme = mode();
|
|
90
|
+
$("#sw-skin-n").textContent = t((SKINS.find((s) => s[0] === skin()) || SKINS[0])[1]);
|
|
91
|
+
$("#sw-mode-n").textContent = t(mode() === "dark" ? "夜间" : "日间");
|
|
92
|
+
}
|
|
93
|
+
$("#sw-skin").addEventListener("click", () => {
|
|
94
|
+
const i = SKINS.findIndex((s) => s[0] === skin());
|
|
95
|
+
localStorage.setItem("orch.boardSkin", SKINS[(i + 1) % SKINS.length][0]);
|
|
96
|
+
applySkin();
|
|
97
|
+
});
|
|
98
|
+
$("#sw-mode").addEventListener("click", () => {
|
|
99
|
+
localStorage.setItem("orch.boardTheme", mode() === "dark" ? "light" : "dark");
|
|
100
|
+
applySkin();
|
|
101
|
+
});
|
|
102
|
+
applySkin();
|
|
103
|
+
|
|
104
|
+
/* ---------------------------------------------------------------- 拉取 */
|
|
105
|
+
let connLost = false;
|
|
106
|
+
|
|
107
|
+
async function api(path) {
|
|
108
|
+
const r = await fetch(path, { headers: { "X-CodeBee-Token": token() } });
|
|
109
|
+
if (r.status === 401) { showGate(t("令牌不正确或已变更")); throw new Error("401"); }
|
|
110
|
+
if (!r.ok) throw new Error("HTTP " + r.status);
|
|
111
|
+
return r.json();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function poll() {
|
|
115
|
+
let data;
|
|
116
|
+
try {
|
|
117
|
+
data = await api("/api/board");
|
|
118
|
+
if (connLost) { connLost = false; $("#hd-conn").classList.add("hidden"); }
|
|
119
|
+
} catch (e) {
|
|
120
|
+
if (e && e.message === "401") return;
|
|
121
|
+
if (!connLost) { connLost = true; $("#hd-conn").classList.remove("hidden"); }
|
|
122
|
+
$("#hd-updated").textContent = t("重试连接中");
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
LAST = data;
|
|
126
|
+
render(data);
|
|
127
|
+
if (DRAWER.runId) syncDrawer();
|
|
128
|
+
}
|
|
129
|
+
let LAST = null;
|
|
130
|
+
|
|
131
|
+
/* ---------------------------------------------------------------- 渲染 */
|
|
132
|
+
const cardEls = new Map(); // task_id -> 卡元素(复用防闪烁)
|
|
133
|
+
|
|
134
|
+
function render(d) {
|
|
135
|
+
const running = Array.isArray(d.running) ? d.running : [];
|
|
136
|
+
const queued = Array.isArray(d.queued) ? d.queued : [];
|
|
137
|
+
const recent = Array.isArray(d.recent) ? d.recent : [];
|
|
138
|
+
const usage = d.usage_today || {};
|
|
139
|
+
const pool = d.pool || {};
|
|
140
|
+
const health = Array.isArray(d.health) ? d.health : [];
|
|
141
|
+
|
|
142
|
+
// 指标条:一格一个数,副位放该指标自己的第二维度(不再重复标签)
|
|
143
|
+
const done = Number(d.today_done) || 0;
|
|
144
|
+
const failed = Number(d.today_failed) || 0;
|
|
145
|
+
const finished = done + failed;
|
|
146
|
+
$("#k-running").textContent = running.length;
|
|
147
|
+
const dSum = running.reduce((a, c) => a + (Number(c.steps_done) || 0), 0);
|
|
148
|
+
const tSum = running.reduce((a, c) => a + (Number(c.steps_total) || 0), 0);
|
|
149
|
+
$("#k-run-sub").textContent = running.length ? t("步 {0}/{1}", dSum, tSum) : t("无活动");
|
|
150
|
+
|
|
151
|
+
$("#k-queued").textContent = queued.length;
|
|
152
|
+
$(".k-queue").classList.toggle("zero", !queued.length);
|
|
153
|
+
$("#k-queue-sub").textContent = queued.length
|
|
154
|
+
? t("最长等待 {0}", fmtDur((Date.now() - parseTs(queued[0].created_at)) / 1000))
|
|
155
|
+
: t("队首空闲");
|
|
156
|
+
|
|
157
|
+
$("#k-done").textContent = done;
|
|
158
|
+
$("#k-done-sub").textContent = finished ? t("成功率 {0}%", Math.round(done / finished * 100)) : t("尚无结课运行");
|
|
159
|
+
$("#k-ratio-fill").style.width = (finished ? done / finished * 100 : 0) + "%";
|
|
160
|
+
|
|
161
|
+
$("#k-failed").textContent = failed;
|
|
162
|
+
$(".k-fail").classList.toggle("zero", !failed);
|
|
163
|
+
const firstFail = recent.find((r) => r.status === "failed");
|
|
164
|
+
$("#k-fail-sub").textContent = failed
|
|
165
|
+
? (firstFail ? t("最近 {0}", hmms(firstFail.ended_at)) : t("需要关注"))
|
|
166
|
+
: t("今日零失败");
|
|
167
|
+
|
|
168
|
+
$("#k-tokens").textContent = fmtTokens(usage.tokens);
|
|
169
|
+
$("#k-use-sub").textContent = fmtCost(usage.cost_usd) + " · " + (Number(usage.calls) || 0) + " " + t("次调用");
|
|
170
|
+
|
|
171
|
+
// 顶栏态势:整屏只在这里说一次「有没有活」
|
|
172
|
+
const st = $("#hd-state");
|
|
173
|
+
st.classList.toggle("live", running.length > 0);
|
|
174
|
+
$("#hd-state-text").textContent = running.length
|
|
175
|
+
? t("蜂群运行中 · {0} 个任务并行", running.length)
|
|
176
|
+
: (queued.length ? t("蜂群待命 · {0} 个任务排队", queued.length) : t("蜂群待命"));
|
|
177
|
+
$("#hd-pool-alive").textContent = pool.alive || 0;
|
|
178
|
+
$("#hd-pool-target").textContent = "/" + (pool.target || 0);
|
|
179
|
+
$("#hd-chat-alive").textContent = pool.chat_alive || 0;
|
|
180
|
+
$("#hd-chat-pool").textContent = "/" + (pool.chat_pool || 0);
|
|
181
|
+
$("#hd-updated").textContent = t("已同步") + " · " +
|
|
182
|
+
(String(d.now || "").slice(11, 19) || new Date().toLocaleTimeString([], { hour12: false }));
|
|
183
|
+
|
|
184
|
+
// 服务健康:常态一行计数,异常才点名(7 个绿灯点阵是噪音)
|
|
185
|
+
const downs = health.filter((h) => h.status === "down" && !h.silenced);
|
|
186
|
+
const hp = $("#hd-health");
|
|
187
|
+
hp.classList.toggle("hidden", !health.length);
|
|
188
|
+
hp.classList.toggle("bad", downs.length > 0);
|
|
189
|
+
$("#hd-health-text").textContent = downs.length
|
|
190
|
+
? t("{0} 个服务异常", downs.length)
|
|
191
|
+
: t("服务 {0}/{1} 正常", health.length, health.length);
|
|
192
|
+
hp.title = downs.length
|
|
193
|
+
? downs.map((h) => (h.provider + " " + (h.model || "")).trim()).join("\n") : "";
|
|
194
|
+
|
|
195
|
+
// 执行池
|
|
196
|
+
const alive = Math.max(0, Number(pool.alive) || 0);
|
|
197
|
+
const target = Math.max(0, Number(pool.target) || 0);
|
|
198
|
+
$("#pool-meter-fill").style.width = (target ? Math.min(100, alive / target * 100) : 0) + "%";
|
|
199
|
+
$("#pool-alive").textContent = alive;
|
|
200
|
+
$("#pool-target").textContent = target;
|
|
201
|
+
$("#chat-alive").textContent = Math.max(0, Number(pool.chat_alive) || 0);
|
|
202
|
+
$("#chat-pool").textContent = Math.max(0, Number(pool.chat_pool) || 0);
|
|
203
|
+
$("#chat-meter-fill").style.width = (pool.chat_pool
|
|
204
|
+
? Math.min(100, (pool.chat_alive || 0) / pool.chat_pool * 100) : 0) + "%";
|
|
205
|
+
$("#pool-available").textContent = Math.max(0, Number(pool.available) || 0);
|
|
206
|
+
$("#pool-mode").textContent = pool.mode || "—";
|
|
207
|
+
|
|
208
|
+
renderWall(d);
|
|
209
|
+
renderTrend(d.trend || [], String(d.now || "").slice(0, 10));
|
|
210
|
+
renderModels(d.models || []);
|
|
211
|
+
renderRecent(recent);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/* 卡片骨架建一次,之后只改字段——4s 重绘不能把终端滚动和动画打断 */
|
|
215
|
+
function buildCard() {
|
|
216
|
+
const el = document.createElement("article");
|
|
217
|
+
el.className = "card";
|
|
218
|
+
el.innerHTML =
|
|
219
|
+
'<div class="c-head"><span class="c-type"></span><span class="c-title"></span>' +
|
|
220
|
+
'<span class="c-elapsed" data-started=""></span></div>' +
|
|
221
|
+
'<div class="c-main"><p class="c-goal"></p><div class="c-steps"></div>' +
|
|
222
|
+
'<div class="c-step-line"><span class="no"></span><span class="ag"></span><span class="sm"></span></div>' +
|
|
223
|
+
'<div class="c-badges"><span class="b b-model"></span><span class="b b-prov"></span></div></div>' +
|
|
224
|
+
'<div class="c-foot"><span class="c-est"><span class="est-track"><i></i></span><em></em></span>' +
|
|
225
|
+
'<span class="c-tok"></span></div>' +
|
|
226
|
+
'<div class="c-live"><div class="c-live-head"><span class="tag"></span><span class="caret"></span></div>' +
|
|
227
|
+
'<div class="c-live-body"><pre class="c-live-text"></pre></div></div>';
|
|
228
|
+
el.addEventListener("click", () => openDrawer(el.dataset.run || "", el));
|
|
229
|
+
return el;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function renderWall(d) {
|
|
233
|
+
const wrap = $("#wall-cards");
|
|
234
|
+
const empty = $("#wall-empty");
|
|
235
|
+
const running = Array.isArray(d.running) ? d.running : [];
|
|
236
|
+
const queued = Array.isArray(d.queued) ? d.queued : [];
|
|
237
|
+
const has = running.length > 0;
|
|
238
|
+
|
|
239
|
+
$("#wall-count").textContent = running.length + " ACTIVE";
|
|
240
|
+
$("#queue-count").textContent = queued.length;
|
|
241
|
+
// toggle 第二参是 force(true=加类),传反会让空态面板在运行时常驻
|
|
242
|
+
empty.classList.toggle("hidden", has);
|
|
243
|
+
// className 整写会抹掉刚 toggle 的 hidden,所以密度与 hidden 一起算
|
|
244
|
+
wrap.className = "wall-grid " +
|
|
245
|
+
(running.length <= 1 ? "d-solo" : running.length <= 3 ? "d-few" : "d-many") +
|
|
246
|
+
(has ? "" : " hidden");
|
|
247
|
+
renderIdle(d);
|
|
248
|
+
|
|
249
|
+
const seen = new Set();
|
|
250
|
+
for (const c of running) {
|
|
251
|
+
seen.add(c.task_id);
|
|
252
|
+
let el = cardEls.get(c.task_id);
|
|
253
|
+
if (!el) { el = buildCard(); wrap.appendChild(el); cardEls.set(c.task_id, el); }
|
|
254
|
+
el.dataset.task = c.task_id;
|
|
255
|
+
el.dataset.run = c.run_id || "";
|
|
256
|
+
|
|
257
|
+
el.querySelector(".c-type").textContent = c.type_name || c.type || t("任务");
|
|
258
|
+
el.querySelector(".c-title").textContent = c.title || t("(未命名)");
|
|
259
|
+
el.querySelector(".c-goal").textContent = c.goal || "";
|
|
260
|
+
const el2 = el.querySelector(".c-elapsed");
|
|
261
|
+
el2.dataset.started = c.started_at || "";
|
|
262
|
+
el2.textContent = fmtDur((Date.now() - parseTs(c.started_at)) / 1000);
|
|
263
|
+
|
|
264
|
+
const cur = c.current || {};
|
|
265
|
+
const seg = el.querySelector(".c-steps");
|
|
266
|
+
const total = Math.max(c.steps_total || 0, 1);
|
|
267
|
+
if (seg.childElementCount !== total) {
|
|
268
|
+
seg.innerHTML = "";
|
|
269
|
+
for (let i = 0; i < total; i++) seg.appendChild(document.createElement("i"));
|
|
270
|
+
}
|
|
271
|
+
[...seg.children].forEach((b, i) => {
|
|
272
|
+
b.className = i < (c.steps_done || 0) ? "d" : (i === (c.steps_done || 0) ? "r" : "");
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
el.querySelector(".c-step-line .no").textContent = cur.n ? ("#" + cur.n) : "—";
|
|
276
|
+
el.querySelector(".c-step-line .ag").textContent = cur.agent || "";
|
|
277
|
+
el.querySelector(".c-step-line .sm").textContent = cur.summary || cur.tail || t("执行中");
|
|
278
|
+
|
|
279
|
+
const bModel = el.querySelector(".b-model");
|
|
280
|
+
bModel.textContent = cur.model || t("模型待定");
|
|
281
|
+
bModel.classList.toggle("dim", !cur.model);
|
|
282
|
+
const bProv = el.querySelector(".b-prov");
|
|
283
|
+
bProv.textContent = cur.provider || "";
|
|
284
|
+
bProv.classList.toggle("hidden", !cur.provider);
|
|
285
|
+
|
|
286
|
+
const cost = Number(c.run_cost_usd) || 0;
|
|
287
|
+
const rt = el.querySelector(".c-tok");
|
|
288
|
+
rt.textContent = (c.run_tokens ? fmtTokens(c.run_tokens) + " tok" : "") +
|
|
289
|
+
(cost > 0 ? (c.run_tokens ? " · " : "") + fmtCost(cost) : "");
|
|
290
|
+
rt.classList.toggle("hidden", !c.run_tokens && !(cost > 0));
|
|
291
|
+
|
|
292
|
+
const est = el.querySelector(".c-est");
|
|
293
|
+
if (c.estimate_s && parseTs(c.started_at)) {
|
|
294
|
+
const p = Math.min(99, Math.round(((Date.now() - parseTs(c.started_at)) / 1000) / c.estimate_s * 100));
|
|
295
|
+
est.classList.remove("hidden");
|
|
296
|
+
est.querySelector(".est-track i").style.width = p + "%";
|
|
297
|
+
est.querySelector("em").textContent = t("预估 {0} · 进度≈{1}%", fmtDur(c.estimate_s), p);
|
|
298
|
+
} else {
|
|
299
|
+
est.classList.add("hidden");
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// 终端框常驻:没有流也留占位,否则卡内多出来的高度无处安放,卡片会塌成一条
|
|
303
|
+
const live = el.querySelector(".c-live");
|
|
304
|
+
const lt = cur.live_text || cur.tail || "";
|
|
305
|
+
const tagMap = { output: t("输出"), thinking: t("思考") };
|
|
306
|
+
live.querySelector(".tag").textContent = tagMap[cur.live_label] || (lt ? t("实时") : "");
|
|
307
|
+
const txt = live.querySelector(".c-live-text");
|
|
308
|
+
txt.textContent = lt || t("等待实时输出…");
|
|
309
|
+
txt.classList.toggle("dim", !lt);
|
|
310
|
+
live.querySelector(".caret").classList.toggle("hidden", !lt);
|
|
311
|
+
if (DRAWER.taskId === c.task_id) el.classList.add("sel");
|
|
312
|
+
}
|
|
313
|
+
for (const [tid, el] of cardEls) {
|
|
314
|
+
if (!seen.has(tid)) { el.remove(); cardEls.delete(tid); }
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const qr = $("#queue-row");
|
|
318
|
+
qr.classList.toggle("hidden", !queued.length);
|
|
319
|
+
$("#queue-chips").innerHTML = queued.map((q) => {
|
|
320
|
+
const wait = q.created_at ? fmtDur((Date.now() - parseTs(q.created_at)) / 1000) : "";
|
|
321
|
+
return '<span class="chip" title="' + esc(q.title) + '">' + esc(q.title) +
|
|
322
|
+
(wait ? "<em>" + esc(wait) + "</em>" : "") + "</span>";
|
|
323
|
+
}).join("");
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/* 空闲时面板不放轨道动画装饰,改画今日运行表——大屏最该回答「今天跑过什么」 */
|
|
327
|
+
function renderIdle(d) {
|
|
328
|
+
const today = String(d.today || "");
|
|
329
|
+
const rows = (Array.isArray(d.recent) ? d.recent : []).filter((r) =>
|
|
330
|
+
String(r.ended_at || "").startsWith(today));
|
|
331
|
+
$("#idle-count").textContent = rows.length;
|
|
332
|
+
const ST = { done: t("完成"), failed: t("失败"), cancelled: t("取消") };
|
|
333
|
+
$("#idle-rows").innerHTML = rows.length ? rows.map((r) =>
|
|
334
|
+
'<div class="h-row"><span class="h-st ' + esc(r.status) + '">' + esc(ST[r.status] || r.status) + "</span>" +
|
|
335
|
+
'<span class="h-t" title="' + esc(r.title) + '">' + esc(r.title) + "</span>" +
|
|
336
|
+
'<span class="h-meta">' + (r.duration_s != null ? fmtDur(r.duration_s) : "—") + "</span>" +
|
|
337
|
+
'<span class="h-time">' + esc(hmms(r.ended_at)) + "</span>" +
|
|
338
|
+
(r.error ? '<span class="h-e" title="' + esc(r.error) + '">' + esc(r.error) + "</span>" : "") +
|
|
339
|
+
"</div>").join("") : '<div class="empty">' + t("今天还没有已结束的") + "</div>";
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function renderTrend(trend, today) {
|
|
343
|
+
const box = $("#trend");
|
|
344
|
+
const max = Math.max(1, ...trend.map((t) => t.tokens || 0));
|
|
345
|
+
const sum = trend.reduce((a, t) => a + (t.tokens || 0), 0);
|
|
346
|
+
$("#trend-total").textContent = fmtTokens(sum) + " / " + t("7日");
|
|
347
|
+
box.innerHTML = trend.map((t) => {
|
|
348
|
+
const h = Math.round((t.tokens || 0) / max * 100);
|
|
349
|
+
const isToday = today && String(t.day || "") === today;
|
|
350
|
+
return '<div class="bar' + (t.tokens ? " on" : "") + (isToday ? " today" : "") +
|
|
351
|
+
'" title="' + esc(t.day) + " · " + fmtTokens(t.tokens) + '"><i style="height:' +
|
|
352
|
+
Math.max(h, 3) + '%"></i><b>' + esc(String(t.day || "").slice(5)) + "</b></div>";
|
|
353
|
+
}).join("");
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function renderModels(models) {
|
|
357
|
+
const box = $("#models");
|
|
358
|
+
if (!models.length) { box.innerHTML = '<div class="empty">' + t("暂无调用记录") + "</div>"; return; }
|
|
359
|
+
const max = Math.max(1, ...models.map((m) => m.tokens || 0));
|
|
360
|
+
const sum = models.reduce((a, m) => a + (m.tokens || 0), 0) || 1;
|
|
361
|
+
box.innerHTML = models.map((m) =>
|
|
362
|
+
'<div class="model-row"><div class="l"><div class="nm" title="' + esc(m.model) + '">' + esc(m.model) + "</div>" +
|
|
363
|
+
'<div class="tk"><i style="width:' + Math.max(3, (m.tokens || 0) / max * 100) + '%"></i></div></div>' +
|
|
364
|
+
'<div class="vl">' + fmtTokens(m.tokens) + "<em>" + Math.round((m.tokens || 0) / sum * 100) + "%</em></div></div>"
|
|
365
|
+
).join("");
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function renderRecent(recent) {
|
|
369
|
+
const box = $("#recent");
|
|
370
|
+
$("#recent-count").textContent = recent.length + " " + t("条");
|
|
371
|
+
if (!recent.length) { box.innerHTML = '<div class="empty">' + t("还没有已完成的运行") + "</div>"; return; }
|
|
372
|
+
box.innerHTML = recent.map((r) => {
|
|
373
|
+
const cls = r.status === "done" ? "done" : r.status === "failed" ? "failed" : "cancelled";
|
|
374
|
+
return '<div class="r-row"><span class="r-dot ' + cls + '"></span>' +
|
|
375
|
+
'<span class="r-t" title="' + esc(r.title + (r.error ? " · " + r.error : "")) + '">' + esc(r.title) + "</span>" +
|
|
376
|
+
'<span class="r-meta">' + (r.duration_s != null ? fmtDur(r.duration_s) : "—") + " · " + esc(hmms(r.ended_at)) + "</span>" +
|
|
377
|
+
(r.error ? '<span class="r-err">' + esc(r.error) + "</span>" : "") + "</div>";
|
|
378
|
+
}).join("");
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/* ---------------------------------------------------------------- 下钻抽屉
|
|
382
|
+
步骤链:/api/board/run?run_id=(点开才拉,不进 4s 轮询)
|
|
383
|
+
单步日志:现成的 /api/runs/<id>/log?step=<rel>&pretty=1
|
|
384
|
+
运行中的步每 2.5s 续拉,结束即停,避免盯着不动的日志以为坏了。 */
|
|
385
|
+
const DRAWER = { taskId: "", runId: "", sel: -1, steps: [], timer: 0 };
|
|
386
|
+
|
|
387
|
+
async function openDrawer(runId, cardEl) {
|
|
388
|
+
if (!runId) return;
|
|
389
|
+
DRAWER.runId = runId;
|
|
390
|
+
DRAWER.taskId = cardEl ? cardEl.dataset.task || "" : "";
|
|
391
|
+
DRAWER.sel = -1;
|
|
392
|
+
document.querySelectorAll(".card.sel").forEach((el) => el.classList.remove("sel"));
|
|
393
|
+
if (cardEl) cardEl.classList.add("sel");
|
|
394
|
+
$("#scrim").classList.add("on");
|
|
395
|
+
$("#drawer").classList.add("on");
|
|
396
|
+
$("#drawer").setAttribute("aria-hidden", "false");
|
|
397
|
+
|
|
398
|
+
const c = ((LAST || {}).running || []).find((x) => x.run_id === runId) || {};
|
|
399
|
+
$("#dw-type").textContent = c.type_name || t("任务");
|
|
400
|
+
$("#dw-title").textContent = c.title || runId;
|
|
401
|
+
$("#dw-goal").textContent = c.goal || "";
|
|
402
|
+
DRAWER.taskId = c.task_id || DRAWER.taskId;
|
|
403
|
+
$("#dw-meta").innerHTML = [
|
|
404
|
+
[t("开始"), String(c.started_at || "").slice(11, 19) || "—"],
|
|
405
|
+
[t("已运行"), c.started_at ? fmtDur((Date.now() - parseTs(c.started_at)) / 1000) : "—"],
|
|
406
|
+
[t("进度"), (c.steps_done || 0) + "/" + (c.steps_total || 0)],
|
|
407
|
+
[t("预估"), c.estimate_s ? fmtDur(c.estimate_s) : t("无")],
|
|
408
|
+
[t("执行体"), (c.current || {}).agent || "—"],
|
|
409
|
+
[t("模型"), (c.current || {}).model || "—"],
|
|
410
|
+
[t("厂商"), (c.current || {}).provider || "—"],
|
|
411
|
+
[t("消耗"), fmtTokens(c.run_tokens) + " · " + fmtCost(c.run_cost_usd)],
|
|
412
|
+
].map(([k, v]) => "<div><span>" + esc(k) + "</span><b>" + esc(v) + "</b></div>").join("");
|
|
413
|
+
|
|
414
|
+
$("#dw-steps").innerHTML = '<div class="empty">' + t("加载中") + "…</div>";
|
|
415
|
+
$("#dw-log-body").textContent = "";
|
|
416
|
+
await syncDrawer();
|
|
417
|
+
clearInterval(DRAWER.timer);
|
|
418
|
+
DRAWER.timer = setInterval(() => {
|
|
419
|
+
if (!DRAWER.runId) return clearInterval(DRAWER.timer);
|
|
420
|
+
syncDrawer();
|
|
421
|
+
}, 2500);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const STEP_ST = () => ({ done: t("完成"), running: t("运行中"), failed: t("失败"),
|
|
425
|
+
cancelled: t("取消"), pending: t("等待") });
|
|
426
|
+
|
|
427
|
+
async function syncDrawer() {
|
|
428
|
+
if (!DRAWER.runId) return;
|
|
429
|
+
let d;
|
|
430
|
+
try { d = await api("/api/board/run?run_id=" + encodeURIComponent(DRAWER.runId)); }
|
|
431
|
+
catch (e) { $("#dw-steps").innerHTML = '<div class="empty">' + t("读取失败") + "</div>"; return; }
|
|
432
|
+
const steps = Array.isArray(d.steps) ? d.steps : [];
|
|
433
|
+
DRAWER.steps = steps;
|
|
434
|
+
if (!steps.length) { $("#dw-steps").innerHTML = '<div class="empty">' + t("暂无步骤") + "</div>"; return; }
|
|
435
|
+
// 未手选时自动跟到运行中的那一步(没有运行步就停到最后一步)
|
|
436
|
+
if (DRAWER.sel < 0) {
|
|
437
|
+
const live = steps.findIndex((s) => s.status === "running");
|
|
438
|
+
DRAWER.sel = live >= 0 ? live : steps.length - 1;
|
|
439
|
+
}
|
|
440
|
+
DRAWER.sel = Math.min(DRAWER.sel, steps.length - 1);
|
|
441
|
+
const ST = STEP_ST();
|
|
442
|
+
$("#dw-steps").innerHTML = steps.map((s, i) =>
|
|
443
|
+
'<div class="s-row' + (i === DRAWER.sel ? " act" : "") + '" data-s="' + i + '">' +
|
|
444
|
+
'<span class="s-i">' + (s.n != null ? s.n : i + 1) + "</span>" +
|
|
445
|
+
'<span class="s-n">' + esc(s.role || s.agent || t("步骤")) +
|
|
446
|
+
(s.status === "running" ? '<i class="s-m"><i></i></i>' : "") + "</span>" +
|
|
447
|
+
'<span class="s-a">' + esc(s.agent_label || s.agent || "—") + "</span>" +
|
|
448
|
+
'<span class="s-d">' + (s.duration_s != null ? fmtDur(s.duration_s) : "—") + "</span>" +
|
|
449
|
+
'<span class="s-s ' + esc(s.status || "") + '">' + esc(ST[s.status] || s.status || "—") + "</span></div>"
|
|
450
|
+
).join("");
|
|
451
|
+
renderStepLog(steps[DRAWER.sel]);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
async function renderStepLog(s) {
|
|
455
|
+
if (!s) return;
|
|
456
|
+
$("#dw-log-tag").textContent = "#" + (s.n != null ? s.n : "") + " " + (s.role || t("步骤"));
|
|
457
|
+
const ST = STEP_ST();
|
|
458
|
+
$("#dw-log-sub").textContent = ST[s.status] || s.status || "";
|
|
459
|
+
const box = $("#dw-log-body");
|
|
460
|
+
if (!s.log) { box.textContent = s.summary || s.error || t("该步没有日志文件"); return; }
|
|
461
|
+
const stick = box.scrollTop + box.clientHeight >= box.scrollHeight - 24;
|
|
462
|
+
try {
|
|
463
|
+
const d = await api("/api/runs/" + encodeURIComponent(DRAWER.runId) + "/log?step=" +
|
|
464
|
+
encodeURIComponent(s.log) + "&pretty=1&tail=20000");
|
|
465
|
+
box.textContent = d.log || t("(日志为空)");
|
|
466
|
+
if (s.status === "running" || stick) box.scrollTop = box.scrollHeight;
|
|
467
|
+
} catch (e) {
|
|
468
|
+
box.textContent = t("日志读取失败");
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function closeDrawer() {
|
|
473
|
+
clearInterval(DRAWER.timer);
|
|
474
|
+
DRAWER.runId = ""; DRAWER.taskId = ""; DRAWER.sel = -1; DRAWER.steps = [];
|
|
475
|
+
$("#scrim").classList.remove("on");
|
|
476
|
+
$("#drawer").classList.remove("on");
|
|
477
|
+
$("#drawer").setAttribute("aria-hidden", "true");
|
|
478
|
+
document.querySelectorAll(".card.sel").forEach((el) => el.classList.remove("sel"));
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
$("#dw-close").addEventListener("click", closeDrawer);
|
|
482
|
+
$("#scrim").addEventListener("click", closeDrawer);
|
|
483
|
+
$("#dw-steps").addEventListener("click", (e) => {
|
|
484
|
+
const row = e.target.closest(".s-row");
|
|
485
|
+
if (!row) return;
|
|
486
|
+
DRAWER.sel = Number(row.dataset.s);
|
|
487
|
+
document.querySelectorAll("#dw-steps .s-row").forEach((el) =>
|
|
488
|
+
el.classList.toggle("act", el === row));
|
|
489
|
+
renderStepLog(DRAWER.steps[DRAWER.sel]);
|
|
490
|
+
});
|
|
491
|
+
document.addEventListener("keydown", (e) => {
|
|
492
|
+
if (e.key === "Escape") closeDrawer();
|
|
493
|
+
if (e.key === "T" || e.key === "t") $("#sw-mode").click();
|
|
494
|
+
if (e.key === "S" || e.key === "s") $("#sw-skin").click();
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
/* ---------------------------------------------------------------- 时钟与耗时每秒跳 */
|
|
498
|
+
setInterval(() => {
|
|
499
|
+
const n = new Date();
|
|
500
|
+
const p = (x) => String(x).padStart(2, "0");
|
|
501
|
+
$("#hd-clock").textContent = p(n.getHours()) + ":" + p(n.getMinutes()) + ":" + p(n.getSeconds());
|
|
502
|
+
document.querySelectorAll(".c-elapsed[data-started]").forEach((el) => {
|
|
503
|
+
const st = parseTs(el.dataset.started);
|
|
504
|
+
if (st) el.textContent = fmtDur((Date.now() - st) / 1000);
|
|
505
|
+
});
|
|
506
|
+
}, 1000);
|
|
507
|
+
|
|
508
|
+
poll();
|
|
509
|
+
setInterval(poll, POLL_MS);
|