codebee 0.1.96 → 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 +8 -0
- package/README.md +3 -2
- package/app/core/agent_context.py +21 -3
- package/app/core/board.py +39 -2
- package/app/core/builtin_agent.py +409 -34
- package/app/core/checkpoints.py +505 -0
- package/app/core/mcp_client.py +200 -29
- package/app/core/pipeline.py +276 -40
- package/app/core/policy.py +36 -4
- package/app/core/project_memory.py +139 -0
- package/app/core/publish/auto.py +80 -19
- package/app/core/publish/browser.py +39 -0
- package/app/core/publish/fanqie.py +2 -0
- package/app/core/publish/flow.py +96 -17
- package/app/core/publish/flows-fanqie-calibrated.json +23 -11
- package/app/core/publish/ledger.py +38 -1
- package/app/core/runner.py +8 -7
- package/app/core/settings.py +13 -5
- package/app/core/settings_schema.py +14 -3
- package/app/core/store.py +21 -6
- package/app/core/tool_outputs.py +107 -0
- package/app/core/wxdigest.py +8 -1
- package/app/main.py +76 -2
- package/app/ui/app.js +356 -82
- package/app/ui/assets/workbench-reference.png +0 -0
- package/app/ui/board.css +451 -666
- package/app/ui/board.html +97 -94
- package/app/ui/board.js +316 -112
- package/app/ui/hive3d.js +242 -0
- package/app/ui/i18n.js +93 -10
- package/app/ui/index.html +31 -3
- package/app/ui/style.css +97 -111
- package/package.json +3 -2
package/app/ui/board.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/* CodeBee 任务驾驶舱(/board.html):4s 轮询 /api/board(KB 级聚合端点),
|
|
2
|
-
不碰 /api/state 全量,也不占 SSE
|
|
2
|
+
不碰 /api/state 全量,也不占 SSE。下钻抽屉按需拉 /api/board/run 与
|
|
3
|
+
/api/runs/<id>/log,不参与 4s 轮询。 */
|
|
3
4
|
"use strict";
|
|
4
5
|
|
|
5
6
|
const $ = (s) => document.querySelector(s);
|
|
@@ -45,6 +46,7 @@ function esc(s) {
|
|
|
45
46
|
return String(s == null ? "" : s).replace(/[&<>"']/g,
|
|
46
47
|
(c) => ({"&": "&", "<": "<", ">": ">", '"': """, "'": "'"}[c]));
|
|
47
48
|
}
|
|
49
|
+
function hmms(s) { return String(s || "").slice(11, 16); }
|
|
48
50
|
|
|
49
51
|
/* ---------------------------------------------------------------- 令牌门 */
|
|
50
52
|
function token() { return localStorage.getItem("orch.token") || ""; }
|
|
@@ -64,24 +66,67 @@ function submitGate() {
|
|
|
64
66
|
poll();
|
|
65
67
|
}
|
|
66
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
|
+
|
|
67
104
|
/* ---------------------------------------------------------------- 拉取 */
|
|
68
105
|
let connLost = false;
|
|
69
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
|
+
|
|
70
114
|
async function poll() {
|
|
71
115
|
let data;
|
|
72
116
|
try {
|
|
73
|
-
|
|
74
|
-
if (r.status === 401) { showGate(t("令牌不正确或已变更")); return; }
|
|
75
|
-
if (!r.ok) throw new Error("HTTP " + r.status);
|
|
76
|
-
data = await r.json();
|
|
117
|
+
data = await api("/api/board");
|
|
77
118
|
if (connLost) { connLost = false; $("#hd-conn").classList.add("hidden"); }
|
|
78
119
|
} catch (e) {
|
|
120
|
+
if (e && e.message === "401") return;
|
|
79
121
|
if (!connLost) { connLost = true; $("#hd-conn").classList.remove("hidden"); }
|
|
80
122
|
$("#hd-updated").textContent = t("重试连接中");
|
|
81
123
|
return;
|
|
82
124
|
}
|
|
125
|
+
LAST = data;
|
|
83
126
|
render(data);
|
|
127
|
+
if (DRAWER.runId) syncDrawer();
|
|
84
128
|
}
|
|
129
|
+
let LAST = null;
|
|
85
130
|
|
|
86
131
|
/* ---------------------------------------------------------------- 渲染 */
|
|
87
132
|
const cardEls = new Map(); // task_id -> 卡元素(复用防闪烁)
|
|
@@ -94,121 +139,129 @@ function render(d) {
|
|
|
94
139
|
const pool = d.pool || {};
|
|
95
140
|
const health = Array.isArray(d.health) ? d.health : [];
|
|
96
141
|
|
|
97
|
-
//
|
|
142
|
+
// 指标条:一格一个数,副位放该指标自己的第二维度(不再重复标签)
|
|
98
143
|
const done = Number(d.today_done) || 0;
|
|
99
144
|
const failed = Number(d.today_failed) || 0;
|
|
100
145
|
const finished = done + failed;
|
|
101
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
|
+
|
|
102
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
|
+
|
|
103
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
|
+
|
|
104
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
|
+
|
|
105
168
|
$("#k-tokens").textContent = fmtTokens(usage.tokens);
|
|
106
|
-
$("#k-
|
|
107
|
-
|
|
108
|
-
// 总态势 Hero:把“有没有活”变成第一眼可读的状态,而不是只重复 KPI。
|
|
109
|
-
const heroActive = running.length > 0;
|
|
110
|
-
$("#hero-live-count").textContent = running.length;
|
|
111
|
-
$("#orb-number").textContent = running.length;
|
|
112
|
-
$("#hero-state").textContent = heroActive ? t("蜂群运行中") : t("蜂群待命");
|
|
113
|
-
if (heroActive) {
|
|
114
|
-
let copy = t("当前有 {0} 个任务正在并行执行", running.length);
|
|
115
|
-
if (queued.length) copy += " · " + t("有 {0} 个任务等待调度", queued.length);
|
|
116
|
-
$("#hero-copy").textContent = copy;
|
|
117
|
-
} else if (queued.length) {
|
|
118
|
-
$("#hero-copy").textContent = t("有 {0} 个任务等待调度", queued.length);
|
|
119
|
-
} else {
|
|
120
|
-
$("#hero-copy").textContent = t("没有正在运行的任务,新任务在主界面创建后会实时出现在这里");
|
|
121
|
-
}
|
|
169
|
+
$("#k-use-sub").textContent = fmtCost(usage.cost_usd) + " · " + (Number(usage.calls) || 0) + " " + t("次调用");
|
|
122
170
|
|
|
123
|
-
//
|
|
124
|
-
$("#hd-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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 个绿灯点阵是噪音)
|
|
128
185
|
const downs = health.filter((h) => h.status === "down" && !h.silenced);
|
|
129
186
|
const hp = $("#hd-health");
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
hp.classList.add("hidden");
|
|
138
|
-
$("#hero-signal").textContent = t("系统状态良好");
|
|
139
|
-
$("#overview").classList.remove("has-alert");
|
|
140
|
-
}
|
|
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") : "";
|
|
141
194
|
|
|
142
|
-
// 执行池
|
|
195
|
+
// 执行池
|
|
143
196
|
const alive = Math.max(0, Number(pool.alive) || 0);
|
|
144
197
|
const target = Math.max(0, Number(pool.target) || 0);
|
|
145
|
-
|
|
146
|
-
$("#pool-meter-fill").style.width = fill + "%";
|
|
198
|
+
$("#pool-meter-fill").style.width = (target ? Math.min(100, alive / target * 100) : 0) + "%";
|
|
147
199
|
$("#pool-alive").textContent = alive;
|
|
148
200
|
$("#pool-target").textContent = target;
|
|
149
|
-
$("#
|
|
150
|
-
$("#
|
|
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 || "—";
|
|
151
207
|
|
|
152
208
|
renderWall(d);
|
|
153
|
-
renderTrend(d.trend || []);
|
|
209
|
+
renderTrend(d.trend || [], String(d.now || "").slice(0, 10));
|
|
154
210
|
renderModels(d.models || []);
|
|
155
211
|
renderRecent(recent);
|
|
156
212
|
}
|
|
157
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
|
+
|
|
158
232
|
function renderWall(d) {
|
|
159
233
|
const wrap = $("#wall-cards");
|
|
160
234
|
const empty = $("#wall-empty");
|
|
161
235
|
const running = Array.isArray(d.running) ? d.running : [];
|
|
162
236
|
const queued = Array.isArray(d.queued) ? d.queued : [];
|
|
163
237
|
const has = running.length > 0;
|
|
238
|
+
|
|
164
239
|
$("#wall-count").textContent = running.length + " ACTIVE";
|
|
165
240
|
$("#queue-count").textContent = queued.length;
|
|
241
|
+
// toggle 第二参是 force(true=加类),传反会让空态面板在运行时常驻
|
|
166
242
|
empty.classList.toggle("hidden", has);
|
|
167
|
-
|
|
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);
|
|
168
248
|
|
|
169
249
|
const seen = new Set();
|
|
170
250
|
for (const c of running) {
|
|
171
251
|
seen.add(c.task_id);
|
|
172
252
|
let el = cardEls.get(c.task_id);
|
|
173
|
-
if (!el) {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
'<div class="card-top"><span class="c-type"></span><span class="c-title"></span>' +
|
|
178
|
-
'<span class="c-elapsed" data-started=""></span></div>' +
|
|
179
|
-
'<div class="c-meta"><span class="b b-model"></span><span class="b b-prov"></span>' +
|
|
180
|
-
'<span class="b b-agent"></span><span class="c-run-tok"></span></div>' +
|
|
181
|
-
'<div class="c-steps"></div>' +
|
|
182
|
-
'<div class="c-stepinfo"><span class="no"></span><span class="ag"></span><span class="sm"></span></div>' +
|
|
183
|
-
'<div class="c-live"><span class="c-live-tag"></span><pre class="c-live-text"></pre></div>' +
|
|
184
|
-
'<div class="c-pct"></div>';
|
|
185
|
-
wrap.appendChild(el);
|
|
186
|
-
cardEls.set(c.task_id, el);
|
|
187
|
-
}
|
|
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
|
+
|
|
188
257
|
el.querySelector(".c-type").textContent = c.type_name || c.type || t("任务");
|
|
189
258
|
el.querySelector(".c-title").textContent = c.title || t("(未命名)");
|
|
259
|
+
el.querySelector(".c-goal").textContent = c.goal || "";
|
|
190
260
|
const el2 = el.querySelector(".c-elapsed");
|
|
191
261
|
el2.dataset.started = c.started_at || "";
|
|
192
262
|
el2.textContent = fmtDur((Date.now() - parseTs(c.started_at)) / 1000);
|
|
193
263
|
|
|
194
|
-
|
|
195
|
-
const cur0 = c.current || {};
|
|
196
|
-
const bModel = el.querySelector(".b-model");
|
|
197
|
-
bModel.textContent = cur0.model || t("模型待定");
|
|
198
|
-
bModel.classList.toggle("dim", !cur0.model);
|
|
199
|
-
const bProv = el.querySelector(".b-prov");
|
|
200
|
-
bProv.textContent = cur0.provider || "";
|
|
201
|
-
bProv.classList.toggle("hidden", !cur0.provider);
|
|
202
|
-
const bAgent = el.querySelector(".b-agent");
|
|
203
|
-
bAgent.textContent = cur0.agent || "";
|
|
204
|
-
bAgent.classList.toggle("hidden", !cur0.agent);
|
|
205
|
-
const rt = el.querySelector(".c-run-tok");
|
|
206
|
-
const cost = Number(c.run_cost_usd) || 0;
|
|
207
|
-
rt.textContent = (c.run_tokens ? fmtTokens(c.run_tokens) + " tok" : "") +
|
|
208
|
-
(cost > 0 ? (c.run_tokens ? " · " : "") + "$" + cost.toFixed(cost >= 1 ? 2 : 3) : "");
|
|
209
|
-
rt.classList.toggle("hidden", !c.run_tokens && !(cost > 0));
|
|
210
|
-
|
|
211
|
-
// 分段进度条
|
|
264
|
+
const cur = c.current || {};
|
|
212
265
|
const seg = el.querySelector(".c-steps");
|
|
213
266
|
const total = Math.max(c.steps_total || 0, 1);
|
|
214
267
|
if (seg.childElementCount !== total) {
|
|
@@ -219,49 +272,84 @@ function renderWall(d) {
|
|
|
219
272
|
b.className = i < (c.steps_done || 0) ? "d" : (i === (c.steps_done || 0) ? "r" : "");
|
|
220
273
|
});
|
|
221
274
|
|
|
222
|
-
|
|
223
|
-
el.querySelector(".
|
|
224
|
-
el.querySelector(".
|
|
225
|
-
el.querySelector(".sm").textContent = cur.summary || t("执行中");
|
|
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("执行中");
|
|
226
278
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
el.querySelector(".c-live-tag").textContent = tagMap[cur.live_label] || t("实时");
|
|
234
|
-
el.querySelector(".c-live-text").textContent = lt;
|
|
235
|
-
}
|
|
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);
|
|
236
285
|
|
|
237
|
-
|
|
238
|
-
const
|
|
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");
|
|
239
293
|
if (c.estimate_s && parseTs(c.started_at)) {
|
|
240
294
|
const p = Math.min(99, Math.round(((Date.now() - parseTs(c.started_at)) / 1000) / c.estimate_s * 100));
|
|
241
|
-
|
|
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);
|
|
242
298
|
} else {
|
|
243
|
-
|
|
299
|
+
est.classList.add("hidden");
|
|
244
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");
|
|
245
312
|
}
|
|
246
313
|
for (const [tid, el] of cardEls) {
|
|
247
314
|
if (!seen.has(tid)) { el.remove(); cardEls.delete(tid); }
|
|
248
315
|
}
|
|
249
316
|
|
|
250
|
-
// 排队条
|
|
251
317
|
const qr = $("#queue-row");
|
|
252
318
|
qr.classList.toggle("hidden", !queued.length);
|
|
253
|
-
$("#queue-chips").innerHTML = queued.map((q) =>
|
|
254
|
-
|
|
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>";
|
|
255
340
|
}
|
|
256
341
|
|
|
257
|
-
function renderTrend(trend) {
|
|
342
|
+
function renderTrend(trend, today) {
|
|
258
343
|
const box = $("#trend");
|
|
259
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日");
|
|
260
347
|
box.innerHTML = trend.map((t) => {
|
|
261
348
|
const h = Math.round((t.tokens || 0) / max * 100);
|
|
262
|
-
const
|
|
263
|
-
return '<div class="bar' + (t.tokens ? "" : "
|
|
264
|
-
|
|
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>";
|
|
265
353
|
}).join("");
|
|
266
354
|
}
|
|
267
355
|
|
|
@@ -269,27 +357,143 @@ function renderModels(models) {
|
|
|
269
357
|
const box = $("#models");
|
|
270
358
|
if (!models.length) { box.innerHTML = '<div class="empty">' + t("暂无调用记录") + "</div>"; return; }
|
|
271
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;
|
|
272
361
|
box.innerHTML = models.map((m) =>
|
|
273
|
-
'<div class="model-row"><
|
|
274
|
-
'<
|
|
275
|
-
'<
|
|
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("");
|
|
276
366
|
}
|
|
277
367
|
|
|
278
368
|
function renderRecent(recent) {
|
|
279
369
|
const box = $("#recent");
|
|
280
|
-
$("#recent-count").textContent = recent.length + " " + t("
|
|
370
|
+
$("#recent-count").textContent = recent.length + " " + t("条");
|
|
281
371
|
if (!recent.length) { box.innerHTML = '<div class="empty">' + t("还没有已完成的运行") + "</div>"; return; }
|
|
282
|
-
const dot = { done: "done", failed: "failed", cancelled: "cancelled" };
|
|
283
372
|
box.innerHTML = recent.map((r) => {
|
|
284
|
-
const cls =
|
|
285
|
-
const t = String(r.ended_at || "").slice(11, 16);
|
|
373
|
+
const cls = r.status === "done" ? "done" : r.status === "failed" ? "failed" : "cancelled";
|
|
286
374
|
return '<div class="r-row"><span class="r-dot ' + cls + '"></span>' +
|
|
287
|
-
'<span class="r-t' + (r.
|
|
288
|
-
|
|
289
|
-
'<span class="r-
|
|
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>";
|
|
290
378
|
}).join("");
|
|
291
379
|
}
|
|
292
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
|
+
|
|
293
497
|
/* ---------------------------------------------------------------- 时钟与耗时每秒跳 */
|
|
294
498
|
setInterval(() => {
|
|
295
499
|
const n = new Date();
|