codebee 0.1.76 → 0.1.78

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 (115) hide show
  1. package/CHANGELOG.md +633 -624
  2. package/LICENSE +21 -21
  3. package/README.md +508 -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/analytics.py +38 -38
  14. package/app/core/artifacts.py +148 -148
  15. package/app/core/assets.py +110 -110
  16. package/app/core/attachments.py +623 -623
  17. package/app/core/audit.py +30 -30
  18. package/app/core/backup.py +470 -470
  19. package/app/core/beekeeper.py +183 -183
  20. package/app/core/benchstore.py +95 -95
  21. package/app/core/board.py +209 -209
  22. package/app/core/bookmeta.py +1036 -1036
  23. package/app/core/bookmeta_catalog.py +180 -180
  24. package/app/core/branching.py +137 -137
  25. package/app/core/builtin_agent.py +1680 -1680
  26. package/app/core/capability.py +144 -144
  27. package/app/core/chaptersafe.py +32 -32
  28. package/app/core/cleanup.py +303 -303
  29. package/app/core/compaction.py +201 -201
  30. package/app/core/contracts.py +255 -255
  31. package/app/core/covergen.py +356 -356
  32. package/app/core/defectretro.py +43 -43
  33. package/app/core/diagnostics.py +114 -114
  34. package/app/core/dispatch.py +190 -190
  35. package/app/core/dispatch_log.py +128 -128
  36. package/app/core/env_scrub.py +85 -85
  37. package/app/core/errorlog.py +137 -137
  38. package/app/core/evaluation.py +174 -174
  39. package/app/core/failure_notify.py +290 -290
  40. package/app/core/goal_service.py +167 -167
  41. package/app/core/health.py +389 -389
  42. package/app/core/history.py +38 -38
  43. package/app/core/hooks.py +162 -162
  44. package/app/core/manager.py +2232 -2232
  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/openapi.py +154 -154
  49. package/app/core/operations.py +228 -228
  50. package/app/core/paihang.py +145 -145
  51. package/app/core/pipeline.py +29 -0
  52. package/app/core/planner.py +88 -0
  53. package/app/core/portguard.py +111 -111
  54. package/app/core/presence.py +77 -77
  55. package/app/core/preview.py +293 -293
  56. package/app/core/prompt_guard.py +78 -78
  57. package/app/core/publish/__init__.py +7 -7
  58. package/app/core/publish/browser.py +655 -655
  59. package/app/core/publish/fanqie.py +165 -165
  60. package/app/core/publish/flow.py +640 -640
  61. package/app/core/publish/flows-fanqie-calibrated.json +336 -336
  62. package/app/core/publish/manager.py +1118 -1118
  63. package/app/core/publish/qimao.py +176 -176
  64. package/app/core/publish/ws.py +139 -139
  65. package/app/core/quality_gate.py +194 -194
  66. package/app/core/redact.py +38 -38
  67. package/app/core/repeat_guard.py +129 -129
  68. package/app/core/retrieval.py +137 -137
  69. package/app/core/router.py +398 -398
  70. package/app/core/runner.py +2238 -2238
  71. package/app/core/selfupdate.py +384 -384
  72. package/app/core/session_log.py +161 -161
  73. package/app/core/sessions.py +312 -312
  74. package/app/core/settings_schema.py +270 -270
  75. package/app/core/share_page.py +114 -114
  76. package/app/core/skill_scan.py +139 -139
  77. package/app/core/skillpacks/market/character-bible.md +66 -66
  78. package/app/core/skillpacks/market/code-risk-checklist.md +58 -58
  79. package/app/core/skillpacks/market/git-workflow.md +57 -57
  80. package/app/core/skillpacks/market/release-notes.md +72 -72
  81. package/app/core/skillpacks/market/weekly-report.md +71 -71
  82. package/app/core/skillpacks/market/worldview-consistency.md +70 -70
  83. package/app/core/skills.py +1 -1
  84. package/app/core/step_runner.py +132 -132
  85. package/app/core/story_tracking.py +275 -275
  86. package/app/core/task_compile.py +199 -199
  87. package/app/core/telemetry.py +292 -292
  88. package/app/core/tlsctx.py +64 -64
  89. package/app/core/token_meter.py +147 -147
  90. package/app/core/tracing.py +144 -144
  91. package/app/core/upstream.py +41 -41
  92. package/app/core/usage.py +1035 -1035
  93. package/app/core/volumes.py +389 -389
  94. package/app/core/wxdigest.py +858 -858
  95. package/app/core/zentao.py +2692 -2692
  96. package/app/mcp_server.py +244 -244
  97. package/app/output/_createbook_body.json +1 -0
  98. package/app/output/_probe_fq_click.py +50 -0
  99. package/app/output/_probe_fq_form.py +42 -0
  100. package/app/output/_probe_fq_modal.py +55 -0
  101. package/app/output/_probe_fq_points.py +71 -0
  102. package/app/output/_probe_fq_submit.py +34 -0
  103. package/app/output/_probe_fq_watch.py +62 -0
  104. package/app/pick_dialog.py +110 -110
  105. package/app/ui/artdiff.js +74 -74
  106. package/app/ui/board.html +162 -162
  107. package/app/ui/board.js +305 -305
  108. package/app/ui/i18n.js +2 -2
  109. package/app/ui/icons/bee.svg +79 -79
  110. package/app/ui/manifest.json +16 -16
  111. package/app/ui/qrcode.js +2297 -2297
  112. package/app/ui/style.css +7086 -7086
  113. package/bin/tutti.js +147 -147
  114. package/package.json +48 -48
  115. 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
@@ -1929,7 +1929,7 @@
1929
1929
  "评分点覆盖": "Scoring coverage",
1930
1930
  "商务清晰度": "Commercial clarity",
1931
1931
  "扫榜选材": "Rank Scan for Topics",
1932
- "抓取七猫/番茄/起点/纵横四平台排行榜公开数据 → AI 提炼跨平台热门题材/人设/差异化切入点(快档直出报告)": "Grabs public rankings from Qimao / Fanqie / Qidian / Zongheng → AI distills cross-platform hot genres / character archetypes / differentiation angles (fast lane, report straight out)",
1932
+ "抓取七猫/番茄/起点/纵横四平台排行榜公开数据 → AI 提炼跨平台热门题材/人设/差异化切入点,每条附 A-E 跨平台证据分级(快档直出报告)": "Grabs public rankings from Qimao / Fanqie / Qidian / Zongheng → AI distills cross-platform hot genres / character archetypes / differentiation angles, each with an A-E cross-platform evidence grade (fast lane, report straight out)",
1933
1933
  "想写哪个方向(一句话,可留空默认分析总榜热门题材)": "Which direction to write (one sentence; leave empty to analyze the overall hot genres)",
1934
1934
  "缺陷复盘": "Defect Retrospective",
1935
1935
  "读取附带的缺陷导出数据 → AI 输出产品/开发/测试三视角复盘报告:整体画像、根因聚类、漏测分析、改进动作(快档直出报告)": "Reads the attached defect export → AI produces a Product/Dev/QA three-lens retrospective: overall profile, root-cause clusters, missed-test analysis, improvement actions (fast lane, report straight out)",
@@ -1970,7 +1970,7 @@
1970
1970
  "按难度动态编排:简单任务走实现+验证短链,复杂任务增加规划、评审、修复与换将": "Orchestrated by difficulty: simple tasks take a short implement-and-verify path; complex ones add planning, review, fixing and agent swapping",
1971
1971
  "CodeBee 直连模型 API 干活(无 CLI 进程),无可用供应商时回退本机 CLI;无拆解/评审(快)": "CodeBee runs directly to the model API (no CLI process); when no usable provider exists, falls back to the local CLI; no decomposition/review (fast)",
1972
1972
  "起草 → 多维评审 → 修订循环 → 发布门禁": "Draft → multi-dim review → revision loop → publish gate",
1973
- "大纲 → 逐章起草 → 每章多维评审修订 → 全局一致性评审 → 合并(可断点续跑)": "Outline → chapter-by-chapter drafting → per-chapter multi-dim review & revision → global consistency review → merge (resumable)",
1973
+ "大纲 → 逐章起草 → 签约质量门禁(开篇/推进/文风/情感/节奏)→ 全局一致性评审 → 合并(可断点续跑)": "Outline → chapter-by-chapter drafting → submission-quality gate (opening / progression / prose / emotion / pacing) → global consistency review → merge (resumable)",
1974
1974
  "公众号/头条风格文章:起草 → 多维评审 → 修订循环 → 发布门禁": "WeChat/Toutiao-style article: draft → multi-dim review → revision loop → publish gate",
1975
1975
  "分镜 + 口播脚本:起草 → 多维评审 → 修订循环 → 发布门禁": "Storyboard + voiceover script: draft → multi-dim review → revision loop → publish gate",
1976
1976
  "周报/月报/述职材料:起草 → 多维评审 → 修订循环 → 发布门禁": "Weekly/monthly/promo material: draft → multi-dim review → revision loop → publish gate",