codebee 0.1.94 → 0.1.96

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 (139) hide show
  1. package/CHANGELOG.md +725 -715
  2. package/LICENSE +21 -21
  3. package/README.md +515 -509
  4. package/app/core/acceptance.py +100 -100
  5. package/app/core/agent_context.py +100 -0
  6. package/app/core/aiflavor.py +197 -197
  7. package/app/core/analytics.py +38 -38
  8. package/app/core/artifacts.py +148 -148
  9. package/app/core/assets.py +110 -110
  10. package/app/core/attachments.py +623 -623
  11. package/app/core/audit.py +30 -30
  12. package/app/core/backup.py +470 -470
  13. package/app/core/beekeeper.py +183 -183
  14. package/app/core/benchstore.py +102 -95
  15. package/app/core/board.py +209 -209
  16. package/app/core/bookmeta.py +1036 -1036
  17. package/app/core/bookmeta_catalog.py +180 -180
  18. package/app/core/branching.py +137 -137
  19. package/app/core/builtin_agent.py +1680 -1680
  20. package/app/core/capability.py +144 -144
  21. package/app/core/chaptersafe.py +32 -32
  22. package/app/core/checkpoints.py +143 -0
  23. package/app/core/cleanup.py +303 -303
  24. package/app/core/compaction.py +201 -201
  25. package/app/core/contracts.py +255 -255
  26. package/app/core/covergen.py +356 -356
  27. package/app/core/defectretro.py +43 -43
  28. package/app/core/diagnostics.py +114 -114
  29. package/app/core/dispatch.py +198 -191
  30. package/app/core/dispatch_log.py +128 -128
  31. package/app/core/env_scrub.py +85 -85
  32. package/app/core/errorlog.py +137 -137
  33. package/app/core/eval_matrix.py +82 -0
  34. package/app/core/evalbench.py +184 -91
  35. package/app/core/evaluation.py +174 -174
  36. package/app/core/failure_notify.py +290 -290
  37. package/app/core/flow_graph.py +33 -0
  38. package/app/core/flows.py +8 -1
  39. package/app/core/goal_service.py +167 -167
  40. package/app/core/health.py +389 -389
  41. package/app/core/history.py +38 -38
  42. package/app/core/hooks.py +162 -162
  43. package/app/core/jobs.py +25 -4
  44. package/app/core/knowledge_pipeline.py +86 -0
  45. package/app/core/manager.py +2232 -2232
  46. package/app/core/mocks.py +64 -64
  47. package/app/core/notify.py +201 -201
  48. package/app/core/novel_quality.py +181 -189
  49. package/app/core/openapi.py +177 -154
  50. package/app/core/operations.py +228 -228
  51. package/app/core/paihang.py +145 -145
  52. package/app/core/pipeline.py +43 -19
  53. package/app/core/planner.py +17 -11
  54. package/app/core/policy.py +52 -0
  55. package/app/core/portguard.py +111 -111
  56. package/app/core/presence.py +77 -77
  57. package/app/core/preview.py +293 -293
  58. package/app/core/prompt_guard.py +78 -78
  59. package/app/core/publish/__init__.py +7 -7
  60. package/app/core/publish/auto.py +26 -3
  61. package/app/core/publish/browser.py +655 -655
  62. package/app/core/publish/chaprows.py +138 -0
  63. package/app/core/publish/fanqie.py +210 -165
  64. package/app/core/publish/flow.py +753 -640
  65. package/app/core/publish/flows-fanqie-calibrated.json +349 -336
  66. package/app/core/publish/ledger.py +13 -1
  67. package/app/core/publish/manager.py +1150 -1118
  68. package/app/core/publish/qimao.py +176 -176
  69. package/app/core/publish/volumes.py +108 -0
  70. package/app/core/publish/ws.py +139 -139
  71. package/app/core/quality_gate.py +194 -194
  72. package/app/core/redact.py +38 -38
  73. package/app/core/repeat_guard.py +129 -129
  74. package/app/core/retrieval.py +137 -137
  75. package/app/core/router.py +398 -398
  76. package/app/core/runner.py +2238 -2238
  77. package/app/core/selfupdate.py +384 -384
  78. package/app/core/session_log.py +161 -161
  79. package/app/core/sessions.py +312 -312
  80. package/app/core/settings.py +20 -0
  81. package/app/core/settings_schema.py +270 -270
  82. package/app/core/share_page.py +114 -114
  83. package/app/core/skill_scan.py +139 -139
  84. package/app/core/skillpacks/fanqie-novel.md +8 -6
  85. package/app/core/skillpacks/market/character-bible.md +66 -66
  86. package/app/core/skillpacks/market/code-risk-checklist.md +58 -58
  87. package/app/core/skillpacks/market/git-workflow.md +57 -57
  88. package/app/core/skillpacks/market/release-notes.md +72 -72
  89. package/app/core/skillpacks/market/weekly-report.md +71 -71
  90. package/app/core/skillpacks/market/worldview-consistency.md +70 -70
  91. package/app/core/skillpacks/qimao-signing.md +14 -11
  92. package/app/core/skills.py +32 -21
  93. package/app/core/step_runner.py +132 -132
  94. package/app/core/store.py +9 -0
  95. package/app/core/story_tracking.py +275 -275
  96. package/app/core/task_compile.py +199 -199
  97. package/app/core/telemetry.py +292 -292
  98. package/app/core/tlsctx.py +64 -64
  99. package/app/core/token_meter.py +147 -147
  100. package/app/core/tracing.py +207 -144
  101. package/app/core/upstream.py +41 -41
  102. package/app/core/usage.py +1038 -1038
  103. package/app/core/volumes.py +389 -389
  104. package/app/core/webhooks.py +89 -0
  105. package/app/core/wxdigest.py +858 -858
  106. package/app/core/zentao.py +2692 -2692
  107. package/app/main.py +104 -9
  108. package/app/mcp_server.py +244 -244
  109. package/app/pick_dialog.py +110 -110
  110. package/app/ui/app.js +287 -19
  111. package/app/ui/artdiff.js +74 -74
  112. package/app/ui/assets/community-qr.png +0 -0
  113. package/app/ui/assets/donate-alipay.jpg +0 -0
  114. package/app/ui/assets/donate-qr.jpg +0 -0
  115. package/app/ui/board.html +162 -162
  116. package/app/ui/board.js +305 -305
  117. package/app/ui/i18n.js +33 -0
  118. package/app/ui/icons/bee.svg +79 -79
  119. package/app/ui/index.html +32 -3
  120. package/app/ui/manifest.json +16 -16
  121. package/app/ui/qrcode.js +2297 -2297
  122. package/app/ui/style.css +196 -2
  123. package/bin/tutti.js +147 -147
  124. package/package.json +48 -48
  125. package/tools/wx_reader.py +261 -261
  126. package/app/.playwright-cli/console-2026-09-23T08-26-03-635Z.log +0 -88
  127. package/app/.playwright-cli/page-2026-09-23T08-26-03-845Z.yml +0 -126
  128. package/app/.playwright-cli/page-2026-09-23T08-29-44-203Z.png +0 -0
  129. package/app/.playwright-cli/page-2026-09-23T08-35-42-046Z.yml +0 -210
  130. package/app/.playwright-cli/page-2026-09-23T08-36-52-858Z.png +0 -0
  131. package/app/.playwright-cli/page-2026-09-23T08-37-33-938Z.png +0 -0
  132. package/app/.playwright-cli/run-detail-after.png +0 -0
  133. package/app/output/_createbook_body.json +0 -1
  134. package/app/output/_probe_fq_click.py +0 -50
  135. package/app/output/_probe_fq_form.py +0 -42
  136. package/app/output/_probe_fq_modal.py +0 -55
  137. package/app/output/_probe_fq_points.py +0 -71
  138. package/app/output/_probe_fq_submit.py +0 -34
  139. package/app/output/_probe_fq_watch.py +0 -62
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);