codebee 0.1.53 → 0.1.54
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 +10 -0
- package/README.md +4 -4
- package/app/core/board.py +209 -0
- package/app/core/chaptersafe.py +32 -0
- package/app/core/jobs.py +1 -1
- package/app/core/pipeline.py +95 -5
- package/app/core/runner.py +82 -70
- package/app/core/share_page.py +4 -1
- package/app/core/store.py +4 -1
- package/app/main.py +4 -1
- package/app/ui/app.js +13403 -13321
- package/app/ui/board.css +292 -0
- package/app/ui/board.html +84 -0
- package/app/ui/board.js +259 -0
- package/app/ui/i18n.js +24 -0
- package/app/ui/index.html +2 -0
- package/app/ui/style.css +17 -0
- package/package.json +1 -1
package/app/ui/board.css
ADDED
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
/* CodeBee 任务驾驶舱(/board.html):挂墙大屏,固定深色科技风。
|
|
2
|
+
色板取自默认皮肤「深海 ocean」,另加蜜蜂琥珀色点缀。 */
|
|
3
|
+
|
|
4
|
+
:root {
|
|
5
|
+
--bg: #0a1220;
|
|
6
|
+
--panel: #111d33;
|
|
7
|
+
--panel2: #16253f;
|
|
8
|
+
--line: #22355433;
|
|
9
|
+
--txt: #dce7f7;
|
|
10
|
+
--dim: #7d90ad;
|
|
11
|
+
--blue: #3ba9f5;
|
|
12
|
+
--violet: #6d6cf6;
|
|
13
|
+
--amber: #ffb84d;
|
|
14
|
+
--green: #35c98c;
|
|
15
|
+
--red: #ff6b6b;
|
|
16
|
+
--mono: "Cascadia Mono", Consolas, ui-monospace, monospace;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
20
|
+
|
|
21
|
+
html, body { height: 100%; }
|
|
22
|
+
|
|
23
|
+
body {
|
|
24
|
+
background:
|
|
25
|
+
radial-gradient(1100px 500px at 70% -10%, #16294a66, transparent 65%),
|
|
26
|
+
radial-gradient(#1c2c4a 1px, transparent 1.5px) 0 0 / 26px 26px,
|
|
27
|
+
var(--bg);
|
|
28
|
+
color: var(--txt);
|
|
29
|
+
font: 15px/1.45 "Segoe UI", "Microsoft YaHei", system-ui, sans-serif;
|
|
30
|
+
overflow: hidden;
|
|
31
|
+
display: flex; flex-direction: column;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.hidden { display: none !important; }
|
|
35
|
+
|
|
36
|
+
/* ---------------------------------------------------------------- 顶栏 */
|
|
37
|
+
#hd {
|
|
38
|
+
height: 58px; flex: none;
|
|
39
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
40
|
+
padding: 0 22px;
|
|
41
|
+
border-bottom: 1px solid var(--line);
|
|
42
|
+
background: linear-gradient(180deg, #14233dcc, #101c31aa);
|
|
43
|
+
}
|
|
44
|
+
.hd-brand { display: flex; align-items: center; gap: 12px; }
|
|
45
|
+
.hd-name { font-size: 22px; font-weight: 700; letter-spacing: 1px; }
|
|
46
|
+
.hd-right { display: flex; align-items: center; gap: 12px; }
|
|
47
|
+
.hd-pill {
|
|
48
|
+
font: 13px var(--mono); color: var(--dim);
|
|
49
|
+
border: 1px solid var(--line); border-radius: 20px;
|
|
50
|
+
padding: 4px 12px; background: #ffffff08;
|
|
51
|
+
}
|
|
52
|
+
.hd-pill.warn { color: #ffd0d0; border-color: #ff6b6b66; background: #ff6b6b14; }
|
|
53
|
+
.hd-pill.off { color: var(--red); border-color: #ff6b6b88; }
|
|
54
|
+
.hd-clock { font: 600 22px var(--mono); color: var(--blue); letter-spacing: 1px; }
|
|
55
|
+
|
|
56
|
+
/* ---------------------------------------------------------------- KPI */
|
|
57
|
+
#kpis {
|
|
58
|
+
flex: none; display: grid;
|
|
59
|
+
grid-template-columns: repeat(6, 1fr); gap: 12px;
|
|
60
|
+
padding: 12px 22px 4px;
|
|
61
|
+
}
|
|
62
|
+
.kpi {
|
|
63
|
+
background: linear-gradient(180deg, var(--panel2), var(--panel));
|
|
64
|
+
border: 1px solid var(--line); border-radius: 12px;
|
|
65
|
+
padding: 12px 16px 10px; position: relative; overflow: hidden;
|
|
66
|
+
}
|
|
67
|
+
.kpi::before {
|
|
68
|
+
content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 3px;
|
|
69
|
+
background: var(--kc, var(--blue)); opacity: .85;
|
|
70
|
+
}
|
|
71
|
+
.kpi { --kc: var(--blue); }
|
|
72
|
+
.k-run { --kc: var(--amber); } .k-queue { --kc: var(--blue); }
|
|
73
|
+
.k-done { --kc: var(--green); } .k-fail { --kc: var(--red); }
|
|
74
|
+
.k-tok { --kc: var(--violet); } .k-cost { --kc: var(--green); }
|
|
75
|
+
.k-num { font: 700 30px/1.1 var(--mono); font-variant-numeric: tabular-nums; }
|
|
76
|
+
.k-fail .k-num { color: var(--red); }
|
|
77
|
+
.k-run .k-num { color: var(--amber); }
|
|
78
|
+
.k-label { margin-top: 3px; font-size: 12.5px; color: var(--dim); letter-spacing: 1px; }
|
|
79
|
+
|
|
80
|
+
/* ---------------------------------------------------------------- 主区 */
|
|
81
|
+
#main {
|
|
82
|
+
flex: 1; min-height: 0;
|
|
83
|
+
display: grid; grid-template-columns: 1fr 380px; gap: 12px;
|
|
84
|
+
padding: 8px 22px 16px;
|
|
85
|
+
}
|
|
86
|
+
.sec-title {
|
|
87
|
+
font-size: 13px; color: var(--dim); letter-spacing: 2px;
|
|
88
|
+
margin-bottom: 8px; display: flex; align-items: center; gap: 8px;
|
|
89
|
+
}
|
|
90
|
+
.sec-title::before { content: ""; width: 8px; height: 8px; border-radius: 2px; background: var(--amber); }
|
|
91
|
+
|
|
92
|
+
#wall { min-height: 0; display: flex; flex-direction: column; }
|
|
93
|
+
#wall-cards {
|
|
94
|
+
flex: 1; min-height: 0; overflow-y: auto;
|
|
95
|
+
display: grid; grid-template-columns: 1fr 1fr; gap: 12px;
|
|
96
|
+
align-content: start; padding-right: 4px;
|
|
97
|
+
}
|
|
98
|
+
#wall-cards::-webkit-scrollbar { width: 6px; }
|
|
99
|
+
#wall-cards::-webkit-scrollbar-thumb { background: #2a3f6299; border-radius: 3px; }
|
|
100
|
+
|
|
101
|
+
/* 运行中任务卡 */
|
|
102
|
+
.card {
|
|
103
|
+
background: linear-gradient(180deg, var(--panel2), var(--panel));
|
|
104
|
+
border: 1px solid var(--line); border-radius: 12px;
|
|
105
|
+
padding: 14px 16px 12px; position: relative; overflow: hidden;
|
|
106
|
+
}
|
|
107
|
+
.card::before {
|
|
108
|
+
content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 4px;
|
|
109
|
+
background: var(--amber);
|
|
110
|
+
animation: pulse 2.2s ease-in-out infinite;
|
|
111
|
+
}
|
|
112
|
+
@keyframes pulse { 50% { opacity: .35; } }
|
|
113
|
+
.card-top { display: flex; align-items: baseline; gap: 10px; }
|
|
114
|
+
.c-type {
|
|
115
|
+
flex: none; font-size: 11.5px; color: var(--amber);
|
|
116
|
+
border: 1px solid #ffb84d55; border-radius: 4px; padding: 1px 7px;
|
|
117
|
+
background: #ffb84d12;
|
|
118
|
+
}
|
|
119
|
+
.c-title {
|
|
120
|
+
font-size: 15.5px; font-weight: 600; white-space: nowrap;
|
|
121
|
+
overflow: hidden; text-overflow: ellipsis; flex: 1;
|
|
122
|
+
}
|
|
123
|
+
.c-elapsed { flex: none; font: 600 14px var(--mono); color: var(--blue); }
|
|
124
|
+
.c-steps {
|
|
125
|
+
margin-top: 10px; display: flex; gap: 4px; align-items: center;
|
|
126
|
+
}
|
|
127
|
+
.c-steps i {
|
|
128
|
+
flex: 1; height: 7px; border-radius: 3px; background: #223554;
|
|
129
|
+
min-width: 6px;
|
|
130
|
+
}
|
|
131
|
+
.c-steps i.d { background: var(--green); }
|
|
132
|
+
.c-steps i.r { background: var(--amber); animation: seg 1.1s ease-in-out infinite; }
|
|
133
|
+
@keyframes seg { 50% { opacity: .4; } }
|
|
134
|
+
.c-stepinfo {
|
|
135
|
+
margin-top: 8px; font-size: 13px; color: var(--txt);
|
|
136
|
+
display: flex; gap: 8px; align-items: baseline; white-space: nowrap; overflow: hidden;
|
|
137
|
+
}
|
|
138
|
+
.c-stepinfo .no { color: var(--amber); font: 600 12.5px var(--mono); flex: none; }
|
|
139
|
+
.c-stepinfo .ag { color: var(--dim); font-size: 12px; flex: none; }
|
|
140
|
+
.c-stepinfo .sm { overflow: hidden; text-overflow: ellipsis; }
|
|
141
|
+
|
|
142
|
+
/* 模型 / 厂商 / 智能体徽章 + 本次消耗 */
|
|
143
|
+
.c-meta {
|
|
144
|
+
margin-top: 9px; display: flex; gap: 6px; align-items: center; overflow: hidden;
|
|
145
|
+
}
|
|
146
|
+
.b {
|
|
147
|
+
flex: none; font: 600 11.5px var(--mono);
|
|
148
|
+
border-radius: 4px; padding: 2px 8px;
|
|
149
|
+
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 180px;
|
|
150
|
+
}
|
|
151
|
+
.b-model { color: #9fd0ff; border: 1px solid #3ba9f566; background: #3ba9f514; }
|
|
152
|
+
.b-prov { color: #c9b8ff; border: 1px solid #6d6cf666; background: #6d6cf614; }
|
|
153
|
+
.b-agent { color: var(--dim); border: 1px solid #7d90ad44; background: #ffffff08; }
|
|
154
|
+
.b.dim { color: #55688c; border-style: dashed; }
|
|
155
|
+
.c-run-tok { flex: none; margin-left: auto; font: 11.5px var(--mono); color: #6fbf9b; }
|
|
156
|
+
|
|
157
|
+
/* 实时思考/输出窗口 */
|
|
158
|
+
.c-live {
|
|
159
|
+
margin-top: 9px; position: relative;
|
|
160
|
+
background: #0c1628; border: 1px solid #22355480; border-radius: 8px;
|
|
161
|
+
padding: 6px 9px 7px;
|
|
162
|
+
}
|
|
163
|
+
.c-live-tag {
|
|
164
|
+
position: absolute; right: 8px; top: 5px;
|
|
165
|
+
font: 600 10px var(--mono); letter-spacing: 1px; color: var(--amber);
|
|
166
|
+
border: 1px solid #ffb84d44; border-radius: 3px; padding: 0 5px;
|
|
167
|
+
background: #ffb84d10;
|
|
168
|
+
}
|
|
169
|
+
.c-live-text {
|
|
170
|
+
margin: 0; font: 11px/1.55 var(--mono); color: #8fb3d9;
|
|
171
|
+
white-space: pre; overflow: hidden; max-height: 6.4em;
|
|
172
|
+
text-overflow: clip;
|
|
173
|
+
}
|
|
174
|
+
.c-pct { margin-top: 8px; font: 11.5px var(--mono); color: var(--dim); }
|
|
175
|
+
|
|
176
|
+
/* 排队行:label 与 chips 垂直居中对齐;多个排队时行内横向滚动 */
|
|
177
|
+
.queue-row {
|
|
178
|
+
flex: none; margin-top: 12px; font-size: 13px;
|
|
179
|
+
display: flex; align-items: center; gap: 12px;
|
|
180
|
+
height: 42px; overflow: hidden;
|
|
181
|
+
}
|
|
182
|
+
.q-label { flex: none; color: var(--blue); }
|
|
183
|
+
#queue-chips {
|
|
184
|
+
flex: 1; min-width: 0;
|
|
185
|
+
display: flex; align-items: center; gap: 8px;
|
|
186
|
+
overflow-x: auto; overflow-y: hidden; padding: 2px 2px 4px;
|
|
187
|
+
}
|
|
188
|
+
#queue-chips::-webkit-scrollbar { height: 5px; }
|
|
189
|
+
#queue-chips::-webkit-scrollbar-thumb { background: #2a3f6299; border-radius: 3px; }
|
|
190
|
+
#queue-chips::-webkit-scrollbar-track { background: transparent; }
|
|
191
|
+
#queue-chips .chip {
|
|
192
|
+
flex: none; display: inline-flex; align-items: center; height: 26px;
|
|
193
|
+
padding: 0 12px;
|
|
194
|
+
border: 1px solid #3ba9f544; border-radius: 13px; color: var(--txt);
|
|
195
|
+
background: #3ba9f510; font-size: 12.5px;
|
|
196
|
+
max-width: 260px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/* 空态 */
|
|
200
|
+
.wall-empty {
|
|
201
|
+
flex: 1; display: flex; flex-direction: column;
|
|
202
|
+
align-items: center; justify-content: center; gap: 10px;
|
|
203
|
+
}
|
|
204
|
+
.idle-bee { width: 110px; height: 110px; opacity: .8; }
|
|
205
|
+
.idle-t1 { font-size: 22px; font-weight: 600; color: var(--dim); letter-spacing: 2px; }
|
|
206
|
+
.idle-t2 { font-size: 13.5px; color: #55688c; }
|
|
207
|
+
|
|
208
|
+
/* ---------------------------------------------------------------- 右栏 */
|
|
209
|
+
#rail { min-height: 0; display: flex; flex-direction: column; gap: 12px; }
|
|
210
|
+
.rail-card {
|
|
211
|
+
background: linear-gradient(180deg, var(--panel2), var(--panel));
|
|
212
|
+
border: 1px solid var(--line); border-radius: 12px;
|
|
213
|
+
padding: 12px 14px;
|
|
214
|
+
}
|
|
215
|
+
.rail-card.grow { flex: 1; min-height: 0; display: flex; flex-direction: column; }
|
|
216
|
+
|
|
217
|
+
/* 7 日趋势(纯 div 柱状) */
|
|
218
|
+
.trend { display: flex; align-items: flex-end; gap: 8px; height: 86px; padding-top: 4px; }
|
|
219
|
+
.trend .bar { flex: 1; display: flex; flex-direction: column; align-items: center; gap: 4px; height: 100%; justify-content: flex-end; }
|
|
220
|
+
.trend .bar i {
|
|
221
|
+
width: 100%; border-radius: 3px 3px 0 0; min-height: 2px;
|
|
222
|
+
background: linear-gradient(180deg, var(--violet), var(--blue));
|
|
223
|
+
opacity: .9;
|
|
224
|
+
}
|
|
225
|
+
.trend .bar.zero i { background: #223554; }
|
|
226
|
+
.trend .bar b { font: 400 10px var(--mono); color: var(--dim); font-weight: 400; }
|
|
227
|
+
|
|
228
|
+
/* 模型 TOP5 */
|
|
229
|
+
.models { display: flex; flex-direction: column; gap: 7px; margin-top: 2px; }
|
|
230
|
+
.model-row { display: grid; grid-template-columns: 118px 1fr 52px; gap: 8px; align-items: center; }
|
|
231
|
+
.model-row .nm { font-size: 12px; color: var(--txt); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
232
|
+
.model-row .tk { height: 8px; border-radius: 4px; background: #223554; overflow: hidden; }
|
|
233
|
+
.model-row .tk i { display: block; height: 100%; border-radius: 4px; background: linear-gradient(90deg, var(--blue), var(--violet)); }
|
|
234
|
+
.model-row .vl { font: 11px var(--mono); color: var(--dim); text-align: right; }
|
|
235
|
+
.models .empty { color: #55688c; font-size: 12.5px; padding: 6px 0; }
|
|
236
|
+
|
|
237
|
+
/* 最近动态 */
|
|
238
|
+
#recent { flex: 1; min-height: 0; overflow-y: auto; }
|
|
239
|
+
#recent::-webkit-scrollbar { width: 6px; }
|
|
240
|
+
#recent::-webkit-scrollbar-thumb { background: #2a3f6299; border-radius: 3px; }
|
|
241
|
+
.r-row {
|
|
242
|
+
display: flex; gap: 9px; align-items: baseline;
|
|
243
|
+
padding: 7px 2px; border-bottom: 1px dashed #22355466; font-size: 13px;
|
|
244
|
+
}
|
|
245
|
+
.r-row:last-child { border-bottom: none; }
|
|
246
|
+
.r-dot { flex: none; width: 8px; height: 8px; border-radius: 50%; margin-top: 4px; }
|
|
247
|
+
.r-dot.done { background: var(--green); box-shadow: 0 0 6px #35c98c88; }
|
|
248
|
+
.r-dot.failed { background: var(--red); box-shadow: 0 0 6px #ff6b6b88; }
|
|
249
|
+
.r-dot.cancelled { background: #55688c; }
|
|
250
|
+
.r-t { flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
251
|
+
.r-t.failed { color: #ffb3b3; }
|
|
252
|
+
.r-d { flex: none; font: 11px var(--mono); color: var(--dim); }
|
|
253
|
+
.recent .empty { color: #55688c; font-size: 12.5px; padding: 6px 0; }
|
|
254
|
+
|
|
255
|
+
/* ---------------------------------------------------------------- 令牌门 */
|
|
256
|
+
.gate {
|
|
257
|
+
position: fixed; inset: 0; z-index: 50;
|
|
258
|
+
background: #0a1220ee; backdrop-filter: blur(3px);
|
|
259
|
+
display: flex; align-items: center; justify-content: center;
|
|
260
|
+
}
|
|
261
|
+
.gate-box { text-align: center; }
|
|
262
|
+
.gate-title { font-size: 20px; font-weight: 700; }
|
|
263
|
+
.gate-sub { color: var(--dim); font-size: 13px; margin: 6px 0 16px; }
|
|
264
|
+
#gate-token {
|
|
265
|
+
display: block; width: 280px; margin: 0 auto 12px;
|
|
266
|
+
background: var(--panel); color: var(--txt);
|
|
267
|
+
border: 1px solid #3a507a; border-radius: 8px;
|
|
268
|
+
padding: 10px 12px; font-size: 15px; outline: none; text-align: center;
|
|
269
|
+
}
|
|
270
|
+
#gate-token:focus { border-color: var(--blue); }
|
|
271
|
+
#gate-ok {
|
|
272
|
+
background: var(--blue); color: #06111f; font-weight: 700;
|
|
273
|
+
border: none; border-radius: 8px; padding: 10px 26px; font-size: 14px; cursor: pointer;
|
|
274
|
+
}
|
|
275
|
+
.gate-err { color: var(--red); font-size: 13px; margin-top: 10px; min-height: 18px; }
|
|
276
|
+
|
|
277
|
+
/* ---------------------------------------------------------------- 自适应 */
|
|
278
|
+
@media (max-width: 1250px) {
|
|
279
|
+
#main { grid-template-columns: 1fr 320px; }
|
|
280
|
+
#wall-cards { grid-template-columns: 1fr; }
|
|
281
|
+
#kpis { grid-template-columns: repeat(3, 1fr); }
|
|
282
|
+
}
|
|
283
|
+
@media (max-width: 900px) {
|
|
284
|
+
body { overflow: auto; }
|
|
285
|
+
#main { grid-template-columns: 1fr; }
|
|
286
|
+
#wall-cards { max-height: none; overflow: visible; }
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/* 大屏挂墙一般关动效的系统偏好也尊重一下 */
|
|
290
|
+
@media (prefers-reduced-motion: reduce) {
|
|
291
|
+
.card::before, .c-steps i.r { animation: none; }
|
|
292
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="zh-CN" data-lang="zh">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
|
6
|
+
<title>CodeBee</title>
|
|
7
|
+
<link rel="icon" href="/icons/icon-192.png">
|
|
8
|
+
<link rel="stylesheet" href="/board.css">
|
|
9
|
+
</head>
|
|
10
|
+
<body>
|
|
11
|
+
|
|
12
|
+
<!-- 远程访问令牌门(本机免输;与主界面共用 orch.token) -->
|
|
13
|
+
<div id="gate" class="gate hidden">
|
|
14
|
+
<div class="gate-box">
|
|
15
|
+
<div class="gate-title" data-i18n="需要访问令牌">需要访问令牌</div>
|
|
16
|
+
<div class="gate-sub" data-i18n="启动 CodeBee 时控制台会显示">启动 CodeBee 时控制台会显示</div>
|
|
17
|
+
<input id="gate-token" type="password" data-i18n-ph="访问令牌" placeholder="访问令牌" autocomplete="off">
|
|
18
|
+
<button id="gate-ok" data-i18n="进入大屏">进入大屏</button>
|
|
19
|
+
<div id="gate-err" class="gate-err"></div>
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
<header id="hd">
|
|
24
|
+
<div class="hd-brand">
|
|
25
|
+
<div class="hd-name">CodeBee</div>
|
|
26
|
+
</div>
|
|
27
|
+
<div class="hd-right">
|
|
28
|
+
<div id="hd-pool" class="hd-pill" title="执行池占用">—</div>
|
|
29
|
+
<div id="hd-health" class="hd-pill warn hidden" title=""></div>
|
|
30
|
+
<div id="hd-conn" class="hd-pill off hidden" data-i18n="连接断开">连接断开</div>
|
|
31
|
+
<div id="hd-clock" class="hd-clock">--:--:--</div>
|
|
32
|
+
</div>
|
|
33
|
+
</header>
|
|
34
|
+
|
|
35
|
+
<section id="kpis">
|
|
36
|
+
<div class="kpi k-run"><div class="k-num" id="k-running">0</div><div class="k-label" data-i18n="运行中">运行中</div></div>
|
|
37
|
+
<div class="kpi k-queue"><div class="k-num" id="k-queued">0</div><div class="k-label" data-i18n="排队中">排队中</div></div>
|
|
38
|
+
<div class="kpi k-done"><div class="k-num" id="k-done">0</div><div class="k-label" data-i18n="今日完成">今日完成</div></div>
|
|
39
|
+
<div class="kpi k-fail"><div class="k-num" id="k-failed">0</div><div class="k-label" data-i18n="今日失败">今日失败</div></div>
|
|
40
|
+
<div class="kpi k-tok"><div class="k-num" id="k-tokens">0</div><div class="k-label" data-i18n="今日 tokens">今日 tokens</div></div>
|
|
41
|
+
<div class="kpi k-cost"><div class="k-num" id="k-cost">$0.00</div><div class="k-label" data-i18n="今日成本">今日成本</div></div>
|
|
42
|
+
</section>
|
|
43
|
+
|
|
44
|
+
<main id="main">
|
|
45
|
+
<section id="wall">
|
|
46
|
+
<div class="sec-title" data-i18n="运行中任务">运行中任务</div>
|
|
47
|
+
<div id="wall-cards"></div>
|
|
48
|
+
<div id="wall-empty" class="wall-empty hidden">
|
|
49
|
+
<svg viewBox="0 0 48 48" class="idle-bee" aria-hidden="true">
|
|
50
|
+
<ellipse cx="24" cy="28" rx="13" ry="16" fill="#3a4a68"/>
|
|
51
|
+
<path d="M11 22h26M11 30h26M13 38h22" stroke="#0d1626" stroke-width="5" stroke-linecap="round"/>
|
|
52
|
+
<ellipse cx="13" cy="12" rx="9" ry="6" fill="#2a3a58" opacity=".8"/>
|
|
53
|
+
<ellipse cx="35" cy="12" rx="9" ry="6" fill="#2a3a58" opacity=".8"/>
|
|
54
|
+
<circle cx="24" cy="11" r="6" fill="#0d1626"/>
|
|
55
|
+
</svg>
|
|
56
|
+
<div class="idle-t1" data-i18n="蜂巢空闲 · 等蜂回巢">蜂巢空闲 · 等蜂回巢</div>
|
|
57
|
+
<div class="idle-t2" data-i18n="没有正在运行的任务,新任务在主界面创建后会实时出现在这里">没有正在运行的任务,新任务在主界面创建后会实时出现在这里</div>
|
|
58
|
+
</div>
|
|
59
|
+
<div id="queue-row" class="queue-row hidden">
|
|
60
|
+
<span class="q-label" data-i18n="排队等待">排队等待</span>
|
|
61
|
+
<span id="queue-chips"></span>
|
|
62
|
+
</div>
|
|
63
|
+
</section>
|
|
64
|
+
|
|
65
|
+
<aside id="rail">
|
|
66
|
+
<div class="rail-card">
|
|
67
|
+
<div class="sec-title" data-i18n="近 7 日 tokens">近 7 日 tokens</div>
|
|
68
|
+
<div id="trend" class="trend"></div>
|
|
69
|
+
</div>
|
|
70
|
+
<div class="rail-card">
|
|
71
|
+
<div class="sec-title" data-i18n="近 7 日模型 TOP5">近 7 日模型 TOP5</div>
|
|
72
|
+
<div id="models" class="models"></div>
|
|
73
|
+
</div>
|
|
74
|
+
<div class="rail-card grow">
|
|
75
|
+
<div class="sec-title" data-i18n="最近动态">最近动态</div>
|
|
76
|
+
<div id="recent" class="recent"></div>
|
|
77
|
+
</div>
|
|
78
|
+
</aside>
|
|
79
|
+
</main>
|
|
80
|
+
|
|
81
|
+
<script src="/i18n.js"></script>
|
|
82
|
+
<script src="/board.js"></script>
|
|
83
|
+
</body>
|
|
84
|
+
</html>
|
package/app/ui/board.js
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
/* CodeBee 任务驾驶舱(/board.html):4s 轮询 /api/board(KB 级聚合端点),
|
|
2
|
+
不碰 /api/state 全量,也不占 SSE。 */
|
|
3
|
+
"use strict";
|
|
4
|
+
|
|
5
|
+
const $ = (s) => document.querySelector(s);
|
|
6
|
+
const t = (k) => (window.t ? window.t(k) : k); // i18n.js 先于本脚本加载
|
|
7
|
+
const POLL_MS = 4000;
|
|
8
|
+
|
|
9
|
+
// URL ?lang=en|zh 可覆盖语言(i18n.js 已按 localStorage 自动 apply 过一次,
|
|
10
|
+
// 这里写回偏好并重刷静态文案;动态文案走 t() 每次渲染实时取)
|
|
11
|
+
try {
|
|
12
|
+
const ql = new URLSearchParams(location.search).get("lang");
|
|
13
|
+
if ((ql === "en" || ql === "zh") && window.getLang && window.getLang() !== ql) {
|
|
14
|
+
window.setLang(ql);
|
|
15
|
+
if (window.applyI18n) window.applyI18n();
|
|
16
|
+
}
|
|
17
|
+
} catch (e) { /* ignore */ }
|
|
18
|
+
|
|
19
|
+
/* ---------------------------------------------------------------- 工具 */
|
|
20
|
+
function fmtTokens(n) {
|
|
21
|
+
n = Number(n) || 0;
|
|
22
|
+
if (n >= 1e8) return (n / 1e8).toFixed(2) + t("亿");
|
|
23
|
+
if (n >= 1e4) return (n / 1e4).toFixed(1) + t("万");
|
|
24
|
+
if (n >= 1e3) return (n / 1e3).toFixed(1) + "k";
|
|
25
|
+
return String(n);
|
|
26
|
+
}
|
|
27
|
+
function fmtCost(n) {
|
|
28
|
+
n = Number(n) || 0;
|
|
29
|
+
if (n >= 100) return "$" + n.toFixed(0);
|
|
30
|
+
if (n >= 1) return "$" + n.toFixed(2);
|
|
31
|
+
return "$" + n.toFixed(3);
|
|
32
|
+
}
|
|
33
|
+
function fmtDur(sec) {
|
|
34
|
+
sec = Math.max(0, Math.floor(Number(sec) || 0));
|
|
35
|
+
const h = Math.floor(sec / 3600), m = Math.floor((sec % 3600) / 60), s = sec % 60;
|
|
36
|
+
if (h) return h + "h" + String(m).padStart(2, "0") + "m";
|
|
37
|
+
return String(m).padStart(2, "0") + ":" + String(s).padStart(2, "0");
|
|
38
|
+
}
|
|
39
|
+
function parseTs(s) {
|
|
40
|
+
// 后端时间格式 "YYYY-MM-DD HH:MM:SS";补 T 让各浏览器 Date 都能解析
|
|
41
|
+
const t = Date.parse(String(s || "").replace(" ", "T"));
|
|
42
|
+
return isNaN(t) ? 0 : t;
|
|
43
|
+
}
|
|
44
|
+
function esc(s) {
|
|
45
|
+
return String(s == null ? "" : s).replace(/[&<>"']/g,
|
|
46
|
+
(c) => ({"&": "&", "<": "<", ">": ">", '"': """, "'": "'"}[c]));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/* ---------------------------------------------------------------- 令牌门 */
|
|
50
|
+
function token() { return localStorage.getItem("orch.token") || ""; }
|
|
51
|
+
|
|
52
|
+
function showGate(msg) {
|
|
53
|
+
$("#gate").classList.remove("hidden");
|
|
54
|
+
$("#gate-err").textContent = msg || "";
|
|
55
|
+
setTimeout(() => $("#gate-token").focus(), 50);
|
|
56
|
+
}
|
|
57
|
+
$("#gate-ok").addEventListener("click", submitGate);
|
|
58
|
+
$("#gate-token").addEventListener("keydown", (e) => { if (e.key === "Enter") submitGate(); });
|
|
59
|
+
function submitGate() {
|
|
60
|
+
const v = $("#gate-token").value.trim();
|
|
61
|
+
if (!v) return;
|
|
62
|
+
localStorage.setItem("orch.token", v);
|
|
63
|
+
$("#gate").classList.add("hidden");
|
|
64
|
+
poll();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/* ---------------------------------------------------------------- 拉取 */
|
|
68
|
+
let connLost = false;
|
|
69
|
+
|
|
70
|
+
async function poll() {
|
|
71
|
+
let data;
|
|
72
|
+
try {
|
|
73
|
+
const r = await fetch("/api/board", { headers: { "X-CodeBee-Token": token() } });
|
|
74
|
+
if (r.status === 401) { showGate(t("令牌不正确或已变更")); return; }
|
|
75
|
+
if (!r.ok) throw new Error("HTTP " + r.status);
|
|
76
|
+
data = await r.json();
|
|
77
|
+
if (connLost) { connLost = false; $("#hd-conn").classList.add("hidden"); }
|
|
78
|
+
} catch (e) {
|
|
79
|
+
if (!connLost) { connLost = true; $("#hd-conn").classList.remove("hidden"); }
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
render(data);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/* ---------------------------------------------------------------- 渲染 */
|
|
86
|
+
const cardEls = new Map(); // task_id -> 卡元素(复用防闪烁)
|
|
87
|
+
|
|
88
|
+
function render(d) {
|
|
89
|
+
// KPI
|
|
90
|
+
$("#k-running").textContent = d.running.length;
|
|
91
|
+
$("#k-queued").textContent = d.queued.length;
|
|
92
|
+
$("#k-done").textContent = d.today_done;
|
|
93
|
+
$("#k-failed").textContent = d.today_failed;
|
|
94
|
+
$("#k-tokens").textContent = fmtTokens(d.usage_today.tokens);
|
|
95
|
+
$("#k-cost").textContent = fmtCost(d.usage_today.cost_usd);
|
|
96
|
+
|
|
97
|
+
// 顶栏
|
|
98
|
+
const pool = d.pool || {};
|
|
99
|
+
$("#hd-pool").textContent = t("蜂巢") + " " + (pool.alive || 0) + "/" + (pool.target || 0)
|
|
100
|
+
+ " · " + t("对话") + " " + (pool.chat_alive || 0) + "/" + (pool.chat_pool || 0);
|
|
101
|
+
const downs = (d.health || []).filter((h) => h.status === "down" && !h.silenced);
|
|
102
|
+
const hp = $("#hd-health");
|
|
103
|
+
if (downs.length) {
|
|
104
|
+
hp.classList.remove("hidden");
|
|
105
|
+
hp.textContent = "⚠ " + downs.map((h) => h.provider).join("、") + " " + t("异常");
|
|
106
|
+
hp.title = downs.map((h) => (h.provider + " " + (h.model || "")).trim()).join("\n");
|
|
107
|
+
} else {
|
|
108
|
+
hp.classList.add("hidden");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
renderWall(d);
|
|
112
|
+
renderTrend(d.trend || []);
|
|
113
|
+
renderModels(d.models || []);
|
|
114
|
+
renderRecent(d.recent || []);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function renderWall(d) {
|
|
118
|
+
const wrap = $("#wall-cards");
|
|
119
|
+
const empty = $("#wall-empty");
|
|
120
|
+
const has = d.running.length > 0;
|
|
121
|
+
empty.classList.toggle("hidden", has);
|
|
122
|
+
wrap.classList.toggle("hidden", !has);
|
|
123
|
+
|
|
124
|
+
const seen = new Set();
|
|
125
|
+
for (const c of d.running) {
|
|
126
|
+
seen.add(c.task_id);
|
|
127
|
+
let el = cardEls.get(c.task_id);
|
|
128
|
+
if (!el) {
|
|
129
|
+
el = document.createElement("div");
|
|
130
|
+
el.className = "card";
|
|
131
|
+
el.innerHTML =
|
|
132
|
+
'<div class="card-top"><span class="c-type"></span><span class="c-title"></span>' +
|
|
133
|
+
'<span class="c-elapsed" data-started=""></span></div>' +
|
|
134
|
+
'<div class="c-meta"><span class="b b-model"></span><span class="b b-prov"></span>' +
|
|
135
|
+
'<span class="b b-agent"></span><span class="c-run-tok"></span></div>' +
|
|
136
|
+
'<div class="c-steps"></div>' +
|
|
137
|
+
'<div class="c-stepinfo"><span class="no"></span><span class="ag"></span><span class="sm"></span></div>' +
|
|
138
|
+
'<div class="c-live"><span class="c-live-tag"></span><pre class="c-live-text"></pre></div>' +
|
|
139
|
+
'<div class="c-pct"></div>';
|
|
140
|
+
wrap.appendChild(el);
|
|
141
|
+
cardEls.set(c.task_id, el);
|
|
142
|
+
}
|
|
143
|
+
el.querySelector(".c-type").textContent = c.type_name || c.type || t("任务");
|
|
144
|
+
el.querySelector(".c-title").textContent = c.title || t("(未命名)");
|
|
145
|
+
const el2 = el.querySelector(".c-elapsed");
|
|
146
|
+
el2.dataset.started = c.started_at || "";
|
|
147
|
+
el2.textContent = fmtDur((Date.now() - parseTs(c.started_at)) / 1000);
|
|
148
|
+
|
|
149
|
+
// 模型 / 厂商 / 执行智能体徽章 + 本次消耗
|
|
150
|
+
const cur0 = c.current || {};
|
|
151
|
+
const bModel = el.querySelector(".b-model");
|
|
152
|
+
bModel.textContent = cur0.model || t("模型待定");
|
|
153
|
+
bModel.classList.toggle("dim", !cur0.model);
|
|
154
|
+
const bProv = el.querySelector(".b-prov");
|
|
155
|
+
bProv.textContent = cur0.provider || "";
|
|
156
|
+
bProv.classList.toggle("hidden", !cur0.provider);
|
|
157
|
+
const bAgent = el.querySelector(".b-agent");
|
|
158
|
+
bAgent.textContent = cur0.agent || "";
|
|
159
|
+
bAgent.classList.toggle("hidden", !cur0.agent);
|
|
160
|
+
const rt = el.querySelector(".c-run-tok");
|
|
161
|
+
const cost = Number(c.run_cost_usd) || 0;
|
|
162
|
+
rt.textContent = (c.run_tokens ? fmtTokens(c.run_tokens) + " tok" : "") +
|
|
163
|
+
(cost > 0 ? (c.run_tokens ? " · " : "") + "$" + cost.toFixed(cost >= 1 ? 2 : 3) : "");
|
|
164
|
+
rt.classList.toggle("hidden", !c.run_tokens && !(cost > 0));
|
|
165
|
+
|
|
166
|
+
// 分段进度条
|
|
167
|
+
const seg = el.querySelector(".c-steps");
|
|
168
|
+
const total = Math.max(c.steps_total || 0, 1);
|
|
169
|
+
if (seg.childElementCount !== total) {
|
|
170
|
+
seg.innerHTML = "";
|
|
171
|
+
for (let i = 0; i < total; i++) seg.appendChild(document.createElement("i"));
|
|
172
|
+
}
|
|
173
|
+
[...seg.children].forEach((b, i) => {
|
|
174
|
+
b.className = i < (c.steps_done || 0) ? "d" : (i === (c.steps_done || 0) ? "r" : "");
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
const cur = c.current || {};
|
|
178
|
+
el.querySelector(".no").textContent = cur.n ? ("#" + cur.n) : "—";
|
|
179
|
+
el.querySelector(".ag").textContent = cur.agent || "";
|
|
180
|
+
el.querySelector(".sm").textContent = cur.summary || t("执行中");
|
|
181
|
+
|
|
182
|
+
// 实时流窗口:思考/输出尾部多行(live_label 是 code,前端翻译)
|
|
183
|
+
const live = el.querySelector(".c-live");
|
|
184
|
+
const lt = cur.live_text || "";
|
|
185
|
+
live.classList.toggle("hidden", !lt);
|
|
186
|
+
if (lt) {
|
|
187
|
+
const tagMap = { output: t("输出"), thinking: t("思考") };
|
|
188
|
+
el.querySelector(".c-live-tag").textContent = tagMap[cur.live_label] || t("实时");
|
|
189
|
+
el.querySelector(".c-live-text").textContent = lt;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// 有预估时长则给完成度百分比
|
|
193
|
+
const pct = el.querySelector(".c-pct");
|
|
194
|
+
if (c.estimate_s && parseTs(c.started_at)) {
|
|
195
|
+
const p = Math.min(99, Math.round(((Date.now() - parseTs(c.started_at)) / 1000) / c.estimate_s * 100));
|
|
196
|
+
pct.textContent = t("预估 {0} · 进度≈{1}%", fmtDur(c.estimate_s), p);
|
|
197
|
+
} else {
|
|
198
|
+
pct.textContent = "";
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
for (const [tid, el] of cardEls) {
|
|
202
|
+
if (!seen.has(tid)) { el.remove(); cardEls.delete(tid); }
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// 排队条
|
|
206
|
+
const qr = $("#queue-row");
|
|
207
|
+
qr.classList.toggle("hidden", !d.queued.length);
|
|
208
|
+
$("#queue-chips").innerHTML = d.queued.map((q) =>
|
|
209
|
+
'<span class="chip" title="' + esc(q.title) + '">' + esc(q.title) + "</span>").join("");
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function renderTrend(trend) {
|
|
213
|
+
const box = $("#trend");
|
|
214
|
+
const max = Math.max(1, ...trend.map((t) => t.tokens || 0));
|
|
215
|
+
box.innerHTML = trend.map((t) => {
|
|
216
|
+
const h = Math.round((t.tokens || 0) / max * 100);
|
|
217
|
+
const label = String(t.day || "").slice(5); // MM-DD
|
|
218
|
+
return '<div class="bar' + (t.tokens ? "" : " zero") + '" title="' + esc(t.day) + " · " +
|
|
219
|
+
fmtTokens(t.tokens) + '"><i style="height:' + Math.max(h, 3) + '%"></i><b>' + esc(label) + "</b></div>";
|
|
220
|
+
}).join("");
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function renderModels(models) {
|
|
224
|
+
const box = $("#models");
|
|
225
|
+
if (!models.length) { box.innerHTML = '<div class="empty">' + t("暂无调用记录") + "</div>"; return; }
|
|
226
|
+
const max = Math.max(1, ...models.map((m) => m.tokens || 0));
|
|
227
|
+
box.innerHTML = models.map((m) =>
|
|
228
|
+
'<div class="model-row"><span class="nm" title="' + esc(m.model) + '">' + esc(m.model) + "</span>" +
|
|
229
|
+
'<span class="tk"><i style="width:' + Math.max(4, Math.round((m.tokens || 0) / max * 100)) + '%"></i></span>' +
|
|
230
|
+
'<span class="vl">' + fmtTokens(m.tokens) + "</span></div>").join("");
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function renderRecent(recent) {
|
|
234
|
+
const box = $("#recent");
|
|
235
|
+
if (!recent.length) { box.innerHTML = '<div class="empty">' + t("还没有已完成的运行") + "</div>"; return; }
|
|
236
|
+
const dot = { done: "done", failed: "failed", cancelled: "cancelled" };
|
|
237
|
+
box.innerHTML = recent.map((r) => {
|
|
238
|
+
const cls = dot[r.status] || "cancelled";
|
|
239
|
+
const t = String(r.ended_at || "").slice(11, 16);
|
|
240
|
+
return '<div class="r-row"><span class="r-dot ' + cls + '"></span>' +
|
|
241
|
+
'<span class="r-t' + (r.status === "failed" ? " failed" : "") + '" title="' +
|
|
242
|
+
esc(r.title + (r.error ? " · " + r.error : "")) + '">' + esc(r.title) + "</span>" +
|
|
243
|
+
'<span class="r-d">' + (r.duration_s != null ? fmtDur(r.duration_s) : "—") + " · " + esc(t) + "</span></div>";
|
|
244
|
+
}).join("");
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/* ---------------------------------------------------------------- 时钟与耗时每秒跳 */
|
|
248
|
+
setInterval(() => {
|
|
249
|
+
const n = new Date();
|
|
250
|
+
const p = (x) => String(x).padStart(2, "0");
|
|
251
|
+
$("#hd-clock").textContent = p(n.getHours()) + ":" + p(n.getMinutes()) + ":" + p(n.getSeconds());
|
|
252
|
+
document.querySelectorAll(".c-elapsed[data-started]").forEach((el) => {
|
|
253
|
+
const st = parseTs(el.dataset.started);
|
|
254
|
+
if (st) el.textContent = fmtDur((Date.now() - st) / 1000);
|
|
255
|
+
});
|
|
256
|
+
}, 1000);
|
|
257
|
+
|
|
258
|
+
poll();
|
|
259
|
+
setInterval(poll, POLL_MS);
|
package/app/ui/i18n.js
CHANGED
|
@@ -266,6 +266,7 @@
|
|
|
266
266
|
"单击:卡内展开 diff;双击:弹窗查看": "Click: expand diff here; double-click: open in popup",
|
|
267
267
|
"双击弹窗查看该文件的变更": "Double-click to view this file's changes in a popup",
|
|
268
268
|
"复制": "Copy",
|
|
269
|
+
"复制日志全文": "Copy full log",
|
|
269
270
|
"已复制": "Copied",
|
|
270
271
|
"已载入原文,可修改后发送": "Original text loaded — edit and send",
|
|
271
272
|
"选择文件夹": "Choose folder",
|
|
@@ -2521,6 +2522,29 @@
|
|
|
2521
2522
|
"【": "[",
|
|
2522
2523
|
"】": "]",
|
|
2523
2524
|
"(已发 ": " (sent ",
|
|
2525
|
+
// —— 任务驾驶舱大屏(board.html)——
|
|
2526
|
+
"运行中任务": "Running tasks",
|
|
2527
|
+
"近 7 日 tokens": "Tokens · last 7 days",
|
|
2528
|
+
"近 7 日模型 TOP5": "Top models · 7 days",
|
|
2529
|
+
"最近动态": "Recent activity",
|
|
2530
|
+
"排队等待": "Queued",
|
|
2531
|
+
"今日完成": "Done today",
|
|
2532
|
+
"今日失败": "Failed today",
|
|
2533
|
+
"今日 tokens": "Tokens today",
|
|
2534
|
+
"今日成本": "Cost today",
|
|
2535
|
+
"思考": "Thinking",
|
|
2536
|
+
"连接断开": "Connection lost",
|
|
2537
|
+
"进入大屏": "Enter board",
|
|
2538
|
+
"模型待定": "Model pending",
|
|
2539
|
+
"异常": "down",
|
|
2540
|
+
"令牌不正确或已变更": "Token incorrect or changed",
|
|
2541
|
+
"执行中": "In progress",
|
|
2542
|
+
"(未命名)": "(untitled)",
|
|
2543
|
+
"预估 {0} · 进度≈{1}%": "Est. {0} · ≈{1}%",
|
|
2544
|
+
"蜂巢空闲 · 等蜂回巢": "Hive idle · waiting for bees",
|
|
2545
|
+
"没有正在运行的任务,新任务在主界面创建后会实时出现在这里": "No running tasks — new ones created in the main UI appear here in real time.",
|
|
2546
|
+
"还没有已完成的运行": "No finished runs yet",
|
|
2547
|
+
"暂无调用记录": "No calls yet",
|
|
2524
2548
|
"未命名": "Untitled",
|
|
2525
2549
|
};
|
|
2526
2550
|
|