codebee 0.1.70 → 0.1.71

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.
Files changed (107) hide show
  1. package/CHANGELOG.md +589 -580
  2. package/LICENSE +21 -21
  3. package/README.md +509 -509
  4. package/app/.playwright-cli/console-2026-09-23T08-26-03-635Z.log +88 -0
  5. package/app/.playwright-cli/page-2026-09-23T08-26-03-845Z.yml +126 -0
  6. package/app/.playwright-cli/page-2026-09-23T08-29-44-203Z.png +0 -0
  7. package/app/.playwright-cli/page-2026-09-23T08-35-42-046Z.yml +210 -0
  8. package/app/.playwright-cli/page-2026-09-23T08-36-52-858Z.png +0 -0
  9. package/app/.playwright-cli/page-2026-09-23T08-37-33-938Z.png +0 -0
  10. package/app/.playwright-cli/run-detail-after.png +0 -0
  11. package/app/core/acceptance.py +100 -100
  12. package/app/core/aiflavor.py +197 -197
  13. package/app/core/artifacts.py +148 -148
  14. package/app/core/assets.py +110 -110
  15. package/app/core/attachments.py +623 -623
  16. package/app/core/audit.py +30 -30
  17. package/app/core/backup.py +470 -470
  18. package/app/core/beekeeper.py +183 -183
  19. package/app/core/benchstore.py +95 -95
  20. package/app/core/board.py +209 -209
  21. package/app/core/bookmeta.py +935 -935
  22. package/app/core/bookmeta_catalog.py +180 -180
  23. package/app/core/branching.py +137 -137
  24. package/app/core/builtin_agent.py +1673 -1673
  25. package/app/core/capability.py +144 -144
  26. package/app/core/chaptersafe.py +32 -32
  27. package/app/core/cleanup.py +303 -303
  28. package/app/core/compaction.py +201 -201
  29. package/app/core/covergen.py +356 -356
  30. package/app/core/defectretro.py +43 -43
  31. package/app/core/diagnostics.py +114 -114
  32. package/app/core/dispatch.py +190 -190
  33. package/app/core/dispatch_log.py +128 -128
  34. package/app/core/env_scrub.py +85 -85
  35. package/app/core/errorlog.py +137 -137
  36. package/app/core/evalbench.py +720 -720
  37. package/app/core/evaluation.py +174 -174
  38. package/app/core/failure_notify.py +290 -290
  39. package/app/core/flows.py +7 -7
  40. package/app/core/goal_service.py +167 -167
  41. package/app/core/health.py +374 -374
  42. package/app/core/history.py +38 -38
  43. package/app/core/hooks.py +162 -162
  44. package/app/core/manager.py +2143 -2143
  45. package/app/core/mocks.py +64 -64
  46. package/app/core/notify.py +201 -201
  47. package/app/core/novel_quality.py +189 -189
  48. package/app/core/operations.py +228 -228
  49. package/app/core/paihang.py +138 -138
  50. package/app/core/portguard.py +111 -111
  51. package/app/core/preview.py +293 -293
  52. package/app/core/publish/__init__.py +7 -7
  53. package/app/core/publish/auto.py +367 -367
  54. package/app/core/publish/browser.py +655 -655
  55. package/app/core/publish/fanqie.py +165 -165
  56. package/app/core/publish/flow.py +589 -589
  57. package/app/core/publish/flows-fanqie-calibrated.json +336 -336
  58. package/app/core/publish/ledger.py +229 -229
  59. package/app/core/publish/manager.py +824 -824
  60. package/app/core/publish/qimao.py +146 -146
  61. package/app/core/publish/ws.py +139 -139
  62. package/app/core/redact.py +38 -38
  63. package/app/core/repeat_guard.py +129 -129
  64. package/app/core/router.py +398 -398
  65. package/app/core/runner.py +2142 -2142
  66. package/app/core/selfupdate.py +384 -384
  67. package/app/core/session_log.py +161 -161
  68. package/app/core/sessions.py +312 -312
  69. package/app/core/settings.py +3 -2
  70. package/app/core/settings_schema.py +270 -270
  71. package/app/core/share_page.py +114 -114
  72. package/app/core/skill_scan.py +122 -122
  73. package/app/core/skillpacks/fanqie-novel.md +80 -80
  74. package/app/core/skillpacks/market/character-bible.md +66 -66
  75. package/app/core/skillpacks/market/code-risk-checklist.md +58 -58
  76. package/app/core/skillpacks/market/git-workflow.md +57 -57
  77. package/app/core/skillpacks/market/release-notes.md +72 -72
  78. package/app/core/skillpacks/market/weekly-report.md +71 -71
  79. package/app/core/skillpacks/market/worldview-consistency.md +70 -70
  80. package/app/core/step_runner.py +132 -132
  81. package/app/core/task_compile.py +199 -199
  82. package/app/core/telemetry.py +292 -292
  83. package/app/core/tlsctx.py +64 -64
  84. package/app/core/token_meter.py +147 -147
  85. package/app/core/tracing.py +144 -144
  86. package/app/core/upstream.py +41 -41
  87. package/app/core/usage.py +1035 -1035
  88. package/app/core/volumes.py +389 -389
  89. package/app/core/wxdigest.py +858 -858
  90. package/app/core/zentao.py +2614 -2614
  91. package/app/main.py +38 -3
  92. package/app/mcp_server.py +175 -175
  93. package/app/pet.py +1 -1
  94. package/app/pick_dialog.py +110 -110
  95. package/app/ui/app.js +11 -4
  96. package/app/ui/artdiff.js +74 -74
  97. package/app/ui/board.html +162 -162
  98. package/app/ui/board.js +305 -305
  99. package/app/ui/i18n.js +1 -1
  100. package/app/ui/icons/bee.svg +79 -79
  101. package/app/ui/manifest.json +16 -16
  102. package/app/ui/qrcode.js +2297 -2297
  103. package/app/ui/style.css +65 -18
  104. package/app//344/275/234/345/223/201/344/277/241/346/201/257-/344/270/203/347/214/253.md +18 -0
  105. package/bin/tutti.js +147 -147
  106. package/package.json +47 -47
  107. package/tools/wx_reader.py +261 -261
package/app/ui/board.js CHANGED
@@ -1,305 +1,305 @@
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 = (...a) => (window.t ? window.t(...a) : a[0]); // i18n.js 先于本脚本加载;透传占位符参数({0}/{1})
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) => ({"&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;"}[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
- $("#hd-updated").textContent = t("重试连接中");
81
- return;
82
- }
83
- render(data);
84
- }
85
-
86
- /* ---------------------------------------------------------------- 渲染 */
87
- const cardEls = new Map(); // task_id -> 卡元素(复用防闪烁)
88
-
89
- function render(d) {
90
- const running = Array.isArray(d.running) ? d.running : [];
91
- const queued = Array.isArray(d.queued) ? d.queued : [];
92
- const recent = Array.isArray(d.recent) ? d.recent : [];
93
- const usage = d.usage_today || {};
94
- const pool = d.pool || {};
95
- const health = Array.isArray(d.health) ? d.health : [];
96
-
97
- // KPI
98
- const done = Number(d.today_done) || 0;
99
- const failed = Number(d.today_failed) || 0;
100
- const finished = done + failed;
101
- $("#k-running").textContent = running.length;
102
- $("#k-queued").textContent = queued.length;
103
- $("#k-done").textContent = done;
104
- $("#k-failed").textContent = failed;
105
- $("#k-tokens").textContent = fmtTokens(usage.tokens);
106
- $("#k-cost").textContent = fmtCost(usage.cost_usd);
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
- }
122
-
123
- // 顶栏
124
- $("#hd-pool").textContent = t("蜂巢") + " " + (pool.alive || 0) + "/" + (pool.target || 0)
125
- + " · " + t("对话") + " " + (pool.chat_alive || 0) + "/" + (pool.chat_pool || 0);
126
- const syncTime = String(d.now || "").slice(11, 19) || new Date().toLocaleTimeString([], { hour12: false });
127
- $("#hd-updated").textContent = t("已同步") + " · " + syncTime;
128
- const downs = health.filter((h) => h.status === "down" && !h.silenced);
129
- const hp = $("#hd-health");
130
- if (downs.length) {
131
- hp.classList.remove("hidden");
132
- hp.textContent = "⚠ " + downs.map((h) => h.provider).join("、") + " " + t("异常");
133
- hp.title = downs.map((h) => (h.provider + " " + (h.model || "")).trim()).join("\n");
134
- $("#hero-signal").textContent = t("有 {0} 个服务异常", downs.length);
135
- $("#overview").classList.add("has-alert");
136
- } else {
137
- hp.classList.add("hidden");
138
- $("#hero-signal").textContent = t("系统状态良好");
139
- $("#overview").classList.remove("has-alert");
140
- }
141
-
142
- // 执行池 / 概览侧栏
143
- const alive = Math.max(0, Number(pool.alive) || 0);
144
- const target = Math.max(0, Number(pool.target) || 0);
145
- const fill = target ? Math.min(100, alive / target * 100) : 0;
146
- $("#pool-meter-fill").style.width = fill + "%";
147
- $("#pool-alive").textContent = alive;
148
- $("#pool-target").textContent = target;
149
- $("#overview-queued").textContent = queued.length;
150
- $("#overview-success").textContent = finished ? Math.round(done / finished * 100) + "%" : "—";
151
-
152
- renderWall(d);
153
- renderTrend(d.trend || []);
154
- renderModels(d.models || []);
155
- renderRecent(recent);
156
- }
157
-
158
- function renderWall(d) {
159
- const wrap = $("#wall-cards");
160
- const empty = $("#wall-empty");
161
- const running = Array.isArray(d.running) ? d.running : [];
162
- const queued = Array.isArray(d.queued) ? d.queued : [];
163
- const has = running.length > 0;
164
- $("#wall-count").textContent = running.length + " ACTIVE";
165
- $("#queue-count").textContent = queued.length;
166
- empty.classList.toggle("hidden", has);
167
- wrap.classList.toggle("hidden", !has);
168
-
169
- const seen = new Set();
170
- for (const c of running) {
171
- seen.add(c.task_id);
172
- let el = cardEls.get(c.task_id);
173
- if (!el) {
174
- el = document.createElement("div");
175
- el.className = "card";
176
- el.innerHTML =
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
- }
188
- el.querySelector(".c-type").textContent = c.type_name || c.type || t("任务");
189
- el.querySelector(".c-title").textContent = c.title || t("(未命名)");
190
- const el2 = el.querySelector(".c-elapsed");
191
- el2.dataset.started = c.started_at || "";
192
- el2.textContent = fmtDur((Date.now() - parseTs(c.started_at)) / 1000);
193
-
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
- // 分段进度条
212
- const seg = el.querySelector(".c-steps");
213
- const total = Math.max(c.steps_total || 0, 1);
214
- if (seg.childElementCount !== total) {
215
- seg.innerHTML = "";
216
- for (let i = 0; i < total; i++) seg.appendChild(document.createElement("i"));
217
- }
218
- [...seg.children].forEach((b, i) => {
219
- b.className = i < (c.steps_done || 0) ? "d" : (i === (c.steps_done || 0) ? "r" : "");
220
- });
221
-
222
- const cur = c.current || {};
223
- el.querySelector(".no").textContent = cur.n ? ("#" + cur.n) : "—";
224
- el.querySelector(".ag").textContent = cur.agent || "";
225
- el.querySelector(".sm").textContent = cur.summary || t("执行中");
226
-
227
- // 实时流窗口:思考/输出尾部多行(live_label 是 code,前端翻译)
228
- const live = el.querySelector(".c-live");
229
- const lt = cur.live_text || "";
230
- live.classList.toggle("hidden", !lt);
231
- if (lt) {
232
- const tagMap = { output: t("输出"), thinking: t("思考") };
233
- el.querySelector(".c-live-tag").textContent = tagMap[cur.live_label] || t("实时");
234
- el.querySelector(".c-live-text").textContent = lt;
235
- }
236
-
237
- // 有预估时长则给完成度百分比
238
- const pct = el.querySelector(".c-pct");
239
- if (c.estimate_s && parseTs(c.started_at)) {
240
- const p = Math.min(99, Math.round(((Date.now() - parseTs(c.started_at)) / 1000) / c.estimate_s * 100));
241
- pct.textContent = t("预估 {0} · 进度≈{1}%", fmtDur(c.estimate_s), p);
242
- } else {
243
- pct.textContent = "";
244
- }
245
- }
246
- for (const [tid, el] of cardEls) {
247
- if (!seen.has(tid)) { el.remove(); cardEls.delete(tid); }
248
- }
249
-
250
- // 排队条
251
- const qr = $("#queue-row");
252
- qr.classList.toggle("hidden", !queued.length);
253
- $("#queue-chips").innerHTML = queued.map((q) =>
254
- '<span class="chip" title="' + esc(q.title) + '">' + esc(q.title) + "</span>").join("");
255
- }
256
-
257
- function renderTrend(trend) {
258
- const box = $("#trend");
259
- const max = Math.max(1, ...trend.map((t) => t.tokens || 0));
260
- box.innerHTML = trend.map((t) => {
261
- const h = Math.round((t.tokens || 0) / max * 100);
262
- const label = String(t.day || "").slice(5); // MM-DD
263
- return '<div class="bar' + (t.tokens ? "" : " zero") + '" title="' + esc(t.day) + " · " +
264
- fmtTokens(t.tokens) + '"><i style="height:' + Math.max(h, 3) + '%"></i><b>' + esc(label) + "</b></div>";
265
- }).join("");
266
- }
267
-
268
- function renderModels(models) {
269
- const box = $("#models");
270
- if (!models.length) { box.innerHTML = '<div class="empty">' + t("暂无调用记录") + "</div>"; return; }
271
- const max = Math.max(1, ...models.map((m) => m.tokens || 0));
272
- box.innerHTML = models.map((m) =>
273
- '<div class="model-row"><span class="nm" title="' + esc(m.model) + '">' + esc(m.model) + "</span>" +
274
- '<span class="tk"><i style="width:' + Math.max(4, Math.round((m.tokens || 0) / max * 100)) + '%"></i></span>' +
275
- '<span class="vl">' + fmtTokens(m.tokens) + "</span></div>").join("");
276
- }
277
-
278
- function renderRecent(recent) {
279
- const box = $("#recent");
280
- $("#recent-count").textContent = recent.length + " " + t("条事件");
281
- if (!recent.length) { box.innerHTML = '<div class="empty">' + t("还没有已完成的运行") + "</div>"; return; }
282
- const dot = { done: "done", failed: "failed", cancelled: "cancelled" };
283
- box.innerHTML = recent.map((r) => {
284
- const cls = dot[r.status] || "cancelled";
285
- const t = String(r.ended_at || "").slice(11, 16);
286
- return '<div class="r-row"><span class="r-dot ' + cls + '"></span>' +
287
- '<span class="r-t' + (r.status === "failed" ? " failed" : "") + '" title="' +
288
- esc(r.title + (r.error ? " · " + r.error : "")) + '">' + esc(r.title) + "</span>" +
289
- '<span class="r-d">' + (r.duration_s != null ? fmtDur(r.duration_s) : "—") + " · " + esc(t) + "</span></div>";
290
- }).join("");
291
- }
292
-
293
- /* ---------------------------------------------------------------- 时钟与耗时每秒跳 */
294
- setInterval(() => {
295
- const n = new Date();
296
- const p = (x) => String(x).padStart(2, "0");
297
- $("#hd-clock").textContent = p(n.getHours()) + ":" + p(n.getMinutes()) + ":" + p(n.getSeconds());
298
- document.querySelectorAll(".c-elapsed[data-started]").forEach((el) => {
299
- const st = parseTs(el.dataset.started);
300
- if (st) el.textContent = fmtDur((Date.now() - st) / 1000);
301
- });
302
- }, 1000);
303
-
304
- poll();
305
- setInterval(poll, POLL_MS);
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 = (...a) => (window.t ? window.t(...a) : a[0]); // i18n.js 先于本脚本加载;透传占位符参数({0}/{1})
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) => ({"&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;"}[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
+ $("#hd-updated").textContent = t("重试连接中");
81
+ return;
82
+ }
83
+ render(data);
84
+ }
85
+
86
+ /* ---------------------------------------------------------------- 渲染 */
87
+ const cardEls = new Map(); // task_id -> 卡元素(复用防闪烁)
88
+
89
+ function render(d) {
90
+ const running = Array.isArray(d.running) ? d.running : [];
91
+ const queued = Array.isArray(d.queued) ? d.queued : [];
92
+ const recent = Array.isArray(d.recent) ? d.recent : [];
93
+ const usage = d.usage_today || {};
94
+ const pool = d.pool || {};
95
+ const health = Array.isArray(d.health) ? d.health : [];
96
+
97
+ // KPI
98
+ const done = Number(d.today_done) || 0;
99
+ const failed = Number(d.today_failed) || 0;
100
+ const finished = done + failed;
101
+ $("#k-running").textContent = running.length;
102
+ $("#k-queued").textContent = queued.length;
103
+ $("#k-done").textContent = done;
104
+ $("#k-failed").textContent = failed;
105
+ $("#k-tokens").textContent = fmtTokens(usage.tokens);
106
+ $("#k-cost").textContent = fmtCost(usage.cost_usd);
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
+ }
122
+
123
+ // 顶栏
124
+ $("#hd-pool").textContent = t("蜂巢") + " " + (pool.alive || 0) + "/" + (pool.target || 0)
125
+ + " · " + t("对话") + " " + (pool.chat_alive || 0) + "/" + (pool.chat_pool || 0);
126
+ const syncTime = String(d.now || "").slice(11, 19) || new Date().toLocaleTimeString([], { hour12: false });
127
+ $("#hd-updated").textContent = t("已同步") + " · " + syncTime;
128
+ const downs = health.filter((h) => h.status === "down" && !h.silenced);
129
+ const hp = $("#hd-health");
130
+ if (downs.length) {
131
+ hp.classList.remove("hidden");
132
+ hp.textContent = "⚠ " + downs.map((h) => h.provider).join("、") + " " + t("异常");
133
+ hp.title = downs.map((h) => (h.provider + " " + (h.model || "")).trim()).join("\n");
134
+ $("#hero-signal").textContent = t("有 {0} 个服务异常", downs.length);
135
+ $("#overview").classList.add("has-alert");
136
+ } else {
137
+ hp.classList.add("hidden");
138
+ $("#hero-signal").textContent = t("系统状态良好");
139
+ $("#overview").classList.remove("has-alert");
140
+ }
141
+
142
+ // 执行池 / 概览侧栏
143
+ const alive = Math.max(0, Number(pool.alive) || 0);
144
+ const target = Math.max(0, Number(pool.target) || 0);
145
+ const fill = target ? Math.min(100, alive / target * 100) : 0;
146
+ $("#pool-meter-fill").style.width = fill + "%";
147
+ $("#pool-alive").textContent = alive;
148
+ $("#pool-target").textContent = target;
149
+ $("#overview-queued").textContent = queued.length;
150
+ $("#overview-success").textContent = finished ? Math.round(done / finished * 100) + "%" : "—";
151
+
152
+ renderWall(d);
153
+ renderTrend(d.trend || []);
154
+ renderModels(d.models || []);
155
+ renderRecent(recent);
156
+ }
157
+
158
+ function renderWall(d) {
159
+ const wrap = $("#wall-cards");
160
+ const empty = $("#wall-empty");
161
+ const running = Array.isArray(d.running) ? d.running : [];
162
+ const queued = Array.isArray(d.queued) ? d.queued : [];
163
+ const has = running.length > 0;
164
+ $("#wall-count").textContent = running.length + " ACTIVE";
165
+ $("#queue-count").textContent = queued.length;
166
+ empty.classList.toggle("hidden", has);
167
+ wrap.classList.toggle("hidden", !has);
168
+
169
+ const seen = new Set();
170
+ for (const c of running) {
171
+ seen.add(c.task_id);
172
+ let el = cardEls.get(c.task_id);
173
+ if (!el) {
174
+ el = document.createElement("div");
175
+ el.className = "card";
176
+ el.innerHTML =
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
+ }
188
+ el.querySelector(".c-type").textContent = c.type_name || c.type || t("任务");
189
+ el.querySelector(".c-title").textContent = c.title || t("(未命名)");
190
+ const el2 = el.querySelector(".c-elapsed");
191
+ el2.dataset.started = c.started_at || "";
192
+ el2.textContent = fmtDur((Date.now() - parseTs(c.started_at)) / 1000);
193
+
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
+ // 分段进度条
212
+ const seg = el.querySelector(".c-steps");
213
+ const total = Math.max(c.steps_total || 0, 1);
214
+ if (seg.childElementCount !== total) {
215
+ seg.innerHTML = "";
216
+ for (let i = 0; i < total; i++) seg.appendChild(document.createElement("i"));
217
+ }
218
+ [...seg.children].forEach((b, i) => {
219
+ b.className = i < (c.steps_done || 0) ? "d" : (i === (c.steps_done || 0) ? "r" : "");
220
+ });
221
+
222
+ const cur = c.current || {};
223
+ el.querySelector(".no").textContent = cur.n ? ("#" + cur.n) : "—";
224
+ el.querySelector(".ag").textContent = cur.agent || "";
225
+ el.querySelector(".sm").textContent = cur.summary || t("执行中");
226
+
227
+ // 实时流窗口:思考/输出尾部多行(live_label 是 code,前端翻译)
228
+ const live = el.querySelector(".c-live");
229
+ const lt = cur.live_text || "";
230
+ live.classList.toggle("hidden", !lt);
231
+ if (lt) {
232
+ const tagMap = { output: t("输出"), thinking: t("思考") };
233
+ el.querySelector(".c-live-tag").textContent = tagMap[cur.live_label] || t("实时");
234
+ el.querySelector(".c-live-text").textContent = lt;
235
+ }
236
+
237
+ // 有预估时长则给完成度百分比
238
+ const pct = el.querySelector(".c-pct");
239
+ if (c.estimate_s && parseTs(c.started_at)) {
240
+ const p = Math.min(99, Math.round(((Date.now() - parseTs(c.started_at)) / 1000) / c.estimate_s * 100));
241
+ pct.textContent = t("预估 {0} · 进度≈{1}%", fmtDur(c.estimate_s), p);
242
+ } else {
243
+ pct.textContent = "";
244
+ }
245
+ }
246
+ for (const [tid, el] of cardEls) {
247
+ if (!seen.has(tid)) { el.remove(); cardEls.delete(tid); }
248
+ }
249
+
250
+ // 排队条
251
+ const qr = $("#queue-row");
252
+ qr.classList.toggle("hidden", !queued.length);
253
+ $("#queue-chips").innerHTML = queued.map((q) =>
254
+ '<span class="chip" title="' + esc(q.title) + '">' + esc(q.title) + "</span>").join("");
255
+ }
256
+
257
+ function renderTrend(trend) {
258
+ const box = $("#trend");
259
+ const max = Math.max(1, ...trend.map((t) => t.tokens || 0));
260
+ box.innerHTML = trend.map((t) => {
261
+ const h = Math.round((t.tokens || 0) / max * 100);
262
+ const label = String(t.day || "").slice(5); // MM-DD
263
+ return '<div class="bar' + (t.tokens ? "" : " zero") + '" title="' + esc(t.day) + " · " +
264
+ fmtTokens(t.tokens) + '"><i style="height:' + Math.max(h, 3) + '%"></i><b>' + esc(label) + "</b></div>";
265
+ }).join("");
266
+ }
267
+
268
+ function renderModels(models) {
269
+ const box = $("#models");
270
+ if (!models.length) { box.innerHTML = '<div class="empty">' + t("暂无调用记录") + "</div>"; return; }
271
+ const max = Math.max(1, ...models.map((m) => m.tokens || 0));
272
+ box.innerHTML = models.map((m) =>
273
+ '<div class="model-row"><span class="nm" title="' + esc(m.model) + '">' + esc(m.model) + "</span>" +
274
+ '<span class="tk"><i style="width:' + Math.max(4, Math.round((m.tokens || 0) / max * 100)) + '%"></i></span>' +
275
+ '<span class="vl">' + fmtTokens(m.tokens) + "</span></div>").join("");
276
+ }
277
+
278
+ function renderRecent(recent) {
279
+ const box = $("#recent");
280
+ $("#recent-count").textContent = recent.length + " " + t("条事件");
281
+ if (!recent.length) { box.innerHTML = '<div class="empty">' + t("还没有已完成的运行") + "</div>"; return; }
282
+ const dot = { done: "done", failed: "failed", cancelled: "cancelled" };
283
+ box.innerHTML = recent.map((r) => {
284
+ const cls = dot[r.status] || "cancelled";
285
+ const t = String(r.ended_at || "").slice(11, 16);
286
+ return '<div class="r-row"><span class="r-dot ' + cls + '"></span>' +
287
+ '<span class="r-t' + (r.status === "failed" ? " failed" : "") + '" title="' +
288
+ esc(r.title + (r.error ? " · " + r.error : "")) + '">' + esc(r.title) + "</span>" +
289
+ '<span class="r-d">' + (r.duration_s != null ? fmtDur(r.duration_s) : "—") + " · " + esc(t) + "</span></div>";
290
+ }).join("");
291
+ }
292
+
293
+ /* ---------------------------------------------------------------- 时钟与耗时每秒跳 */
294
+ setInterval(() => {
295
+ const n = new Date();
296
+ const p = (x) => String(x).padStart(2, "0");
297
+ $("#hd-clock").textContent = p(n.getHours()) + ":" + p(n.getMinutes()) + ":" + p(n.getSeconds());
298
+ document.querySelectorAll(".c-elapsed[data-started]").forEach((el) => {
299
+ const st = parseTs(el.dataset.started);
300
+ if (st) el.textContent = fmtDur((Date.now() - st) / 1000);
301
+ });
302
+ }, 1000);
303
+
304
+ poll();
305
+ setInterval(poll, POLL_MS);
package/app/ui/i18n.js CHANGED
@@ -1301,7 +1301,7 @@
1301
1301
  "启用中": "Enabled",
1302
1302
  "设为主模型": "Set as primary model",
1303
1303
  "单智能体直达(快)": "Single-agent direct (fast)",
1304
- "抓双平台榜 → AI 选题洞察": "Scan dual-platform rankings → AI topic insights",
1304
+ "抓四平台榜 → AI 选题洞察": "Scan four-platform rankings → AI topic insights",
1305
1305
  "新增函数补用例": "Add tests for new functions",
1306
1306
  "同类加函数任务,只要verify_pass=false即不得因review_pass=true或高分判定通过;必须将verify失败原因作为修复输入。": "For similar add-a-function tasks, verify_pass=false blocks acceptance regardless of review_pass or high scores; the verify failure details are the input to the next fix.",
1307
1307
  "凡主角获取越权信息或关键证据(系统记录、录音、账目),必须当章或前文落实来源链(人脉、留底、委托调查),并让角色当场追问一句'东西哪来的';无来源的特权查询与来历不明的证据一律禁止上稿。": "Whenever the protagonist obtains privileged information or key evidence (system records, recordings, ledgers), establish the source chain in the same or an earlier chapter and have a character ask \"where did this come from\" on the spot; unsourced privileged queries and evidence of unknown origin are banned from the manuscript.",