codebee 0.1.96 → 0.1.98
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/README.md +4 -2
- package/app/core/agent_context.py +21 -3
- package/app/core/board.py +39 -2
- package/app/core/builtin_agent.py +409 -34
- package/app/core/checkpoints.py +505 -0
- package/app/core/jobs.py +8 -1
- package/app/core/manager.py +109 -11
- package/app/core/mcp_client.py +200 -29
- package/app/core/pipeline.py +289 -40
- package/app/core/policy.py +36 -4
- package/app/core/project_memory.py +139 -0
- package/app/core/publish/auto.py +80 -19
- package/app/core/publish/browser.py +39 -0
- package/app/core/publish/fanqie.py +2 -0
- package/app/core/publish/flow.py +96 -17
- package/app/core/publish/flows-fanqie-calibrated.json +23 -11
- package/app/core/publish/ledger.py +38 -1
- package/app/core/runner.py +21 -11
- package/app/core/settings.py +13 -5
- package/app/core/settings_schema.py +14 -3
- package/app/core/store.py +21 -6
- package/app/core/tool_outputs.py +107 -0
- package/app/core/wxdigest.py +8 -1
- package/app/main.py +76 -2
- package/app/ui/app.js +356 -82
- package/app/ui/assets/workbench-reference.png +0 -0
- package/app/ui/board.css +451 -666
- package/app/ui/board.html +97 -94
- package/app/ui/board.js +316 -112
- package/app/ui/hive3d.js +242 -0
- package/app/ui/i18n.js +93 -10
- package/app/ui/index.html +31 -3
- package/app/ui/style.css +97 -111
- package/package.json +3 -2
package/app/ui/app.js
CHANGED
|
@@ -4696,12 +4696,127 @@ function renderTaskDetail() {
|
|
|
4696
4696
|
setResumeNowBtn(null); // 退避与否要看 run 数据,先收起防上一个任务残留
|
|
4697
4697
|
const isTask = ((S.state || {}).tasks || []).some((t2) => t2.id === key);
|
|
4698
4698
|
if (isTask) {
|
|
4699
|
+
renderProjectMemoryControls(key);
|
|
4700
|
+
renderCheckpointControls(latestSummary.id || "", true);
|
|
4701
|
+
refreshCheckpointRestoreAvailability(latestSummary);
|
|
4702
|
+
if (latestSummary.id && S._checkpointRun !== latestSummary.id) {
|
|
4703
|
+
S._checkpointRun = latestSummary.id;
|
|
4704
|
+
loadRunCheckpoints(latestSummary.id);
|
|
4705
|
+
}
|
|
4699
4706
|
loadTaskRuns(key, latestSummary);
|
|
4700
4707
|
return;
|
|
4701
4708
|
}
|
|
4702
4709
|
drawTaskDetail(key, ((S.state || {}).runs || []).filter((r) => (r.task_id || r.id) === key));
|
|
4703
4710
|
}
|
|
4704
4711
|
|
|
4712
|
+
function renderCheckpointControls(runId, visible) {
|
|
4713
|
+
const box = $("rd-checkpoints");
|
|
4714
|
+
if (!box) return;
|
|
4715
|
+
box.classList.toggle("hidden", !visible || !runId);
|
|
4716
|
+
if (runId && box.dataset.runId !== runId) {
|
|
4717
|
+
box.dataset.runId = runId;
|
|
4718
|
+
box.dataset.hasRestorable = "false";
|
|
4719
|
+
const restore = $("rd-checkpoints-restore");
|
|
4720
|
+
if (restore) restore.classList.add("hidden");
|
|
4721
|
+
box.classList.add("needs-load");
|
|
4722
|
+
}
|
|
4723
|
+
}
|
|
4724
|
+
|
|
4725
|
+
function refreshCheckpointRestoreAvailability(run) {
|
|
4726
|
+
const box = $("rd-checkpoints");
|
|
4727
|
+
const button = $("rd-checkpoints-restore");
|
|
4728
|
+
if (!box || !button) return;
|
|
4729
|
+
const terminal = !!run && ["done", "failed", "cancelled", "timeout"].includes(run.status);
|
|
4730
|
+
button.classList.toggle("hidden", !terminal || box.dataset.hasRestorable !== "true");
|
|
4731
|
+
}
|
|
4732
|
+
|
|
4733
|
+
function renderProjectMemoryControls(taskId) {
|
|
4734
|
+
const box = $("rd-project-memory");
|
|
4735
|
+
if (!box) return;
|
|
4736
|
+
const task = ((S.state || {}).tasks || []).find((item) => item.id === taskId);
|
|
4737
|
+
box.classList.toggle("hidden", !task || !task.workdir);
|
|
4738
|
+
if (task && task.workdir && S._projectMemoryTask !== taskId) {
|
|
4739
|
+
S._projectMemoryTask = taskId;
|
|
4740
|
+
loadProjectMemory(taskId);
|
|
4741
|
+
}
|
|
4742
|
+
}
|
|
4743
|
+
|
|
4744
|
+
window.loadProjectMemory = async function (taskId) {
|
|
4745
|
+
taskId = taskId || S.detailTaskKey || ((S.state || {}).tasks || []).find((item) => item.id === S.lastRunTask?.id)?.id;
|
|
4746
|
+
if (!taskId) return;
|
|
4747
|
+
const requestSeq = S._projectMemorySeq = (S._projectMemorySeq || 0) + 1;
|
|
4748
|
+
const box = $("rd-project-memory-body");
|
|
4749
|
+
if (!box) return;
|
|
4750
|
+
box.textContent = t("正在加载项目记忆…");
|
|
4751
|
+
try {
|
|
4752
|
+
const data = await api("/api/tasks/" + encodeURIComponent(taskId) + "/memory");
|
|
4753
|
+
if (requestSeq !== S._projectMemorySeq) return;
|
|
4754
|
+
if (S.detailTaskKey && S.detailTaskKey !== taskId) return;
|
|
4755
|
+
const entries = data.entries || [];
|
|
4756
|
+
if (!entries.length) { box.innerHTML = '<span class="hint">' + esc(t("暂无项目记忆")) + "</span>"; return; }
|
|
4757
|
+
box.innerHTML = entries.map((entry) => {
|
|
4758
|
+
const canApprove = entry.status === "pending" && !entry.expired && entry.verified;
|
|
4759
|
+
const canRestore = entry.status === "archived" && !entry.expired && entry.verified;
|
|
4760
|
+
const actions = (canApprove ? '<button type="button" class="ghost small" onclick="decideProjectMemory(\'' + jsq(taskId) + '\',\'' + jsq(entry.id) + '\',\'approve\',' + Number(entry.version) + ')">' + esc(t("批准")) + "</button><button type=\"button\" class=\"ghost small\" onclick=\"decideProjectMemory('" + jsq(taskId) + "','" + jsq(entry.id) + "','reject'," + Number(entry.version) + ')\">' + esc(t("拒绝")) + "</button>" : "") +
|
|
4761
|
+
(entry.status === "approved" ? '<button type="button" class="ghost small" onclick="decideProjectMemory(\'' + jsq(taskId) + '\',\'' + jsq(entry.id) + '\',\'archive\',' + Number(entry.version) + ')">' + esc(t("归档")) + "</button>" : "") +
|
|
4762
|
+
(canRestore ? '<button type="button" class="ghost small" onclick="decideProjectMemory(\'' + jsq(taskId) + '\',\'' + jsq(entry.id) + '\',\'restore\',' + Number(entry.version) + ')">' + esc(t("恢复")) + "</button>" : "");
|
|
4763
|
+
return '<article class="rd-memory-row"><div><strong>' + esc(entry.title || t("项目记忆")) + '</strong><span class="chip">' + esc(entry.status || "") + (entry.expired ? " · " + esc(t("已过期")) : "") + '</span></div><pre>' + esc(entry.content || "") + '</pre><div class="rd-memory-actions">' + actions + "</div></article>";
|
|
4764
|
+
}).join("");
|
|
4765
|
+
} catch (error) { box.innerHTML = '<span class="hint">' + esc(t("加载项目记忆失败:")) + esc(error.message) + "</span>"; }
|
|
4766
|
+
};
|
|
4767
|
+
|
|
4768
|
+
window.decideProjectMemory = async function (taskId, id, action, version) {
|
|
4769
|
+
try {
|
|
4770
|
+
await api("/api/tasks/" + encodeURIComponent(taskId) + "/memory", {
|
|
4771
|
+
method: "POST", headers: Object.assign({ "Content-Type": "application/json" }, authHeaders()),
|
|
4772
|
+
body: JSON.stringify({ id, action, expected_version: version }),
|
|
4773
|
+
});
|
|
4774
|
+
await loadProjectMemory(taskId);
|
|
4775
|
+
} catch (error) { toast(t("项目记忆操作失败:") + error.message, true); }
|
|
4776
|
+
};
|
|
4777
|
+
|
|
4778
|
+
window.loadRunCheckpoints = async function (requestedRunId) {
|
|
4779
|
+
const runId = requestedRunId || S.detailRunId || ((S.state || {}).task_latest || {})[S.detailTaskKey]?.id;
|
|
4780
|
+
if (!runId) return;
|
|
4781
|
+
const requestSeq = S._checkpointRequestSeq = (S._checkpointRequestSeq || 0) + 1;
|
|
4782
|
+
const body = $("rd-checkpoints-body");
|
|
4783
|
+
const box = $("rd-checkpoints");
|
|
4784
|
+
box.classList.remove("hidden");
|
|
4785
|
+
body.textContent = t("正在读取文件检查点…");
|
|
4786
|
+
try {
|
|
4787
|
+
const data = await api("/api/runs/" + encodeURIComponent(runId) + "/checkpoints");
|
|
4788
|
+
if (requestSeq !== S._checkpointRequestSeq) return;
|
|
4789
|
+
if (S.detailRunId && S.detailRunId !== runId) return;
|
|
4790
|
+
if (S.detailTaskKey) {
|
|
4791
|
+
const current = ((S.state || {}).task_latest || {})[S.detailTaskKey];
|
|
4792
|
+
if (current && current.id !== runId) return;
|
|
4793
|
+
}
|
|
4794
|
+
const files = data.files && data.files.files || [];
|
|
4795
|
+
const restore = $("rd-checkpoints-restore");
|
|
4796
|
+
const latest = S.detailTaskKey && ((S.state || {}).task_latest || {})[S.detailTaskKey];
|
|
4797
|
+
const run = (S.lastRun && S.lastRun.id === runId) ? S.lastRun : latest;
|
|
4798
|
+
box.dataset.hasRestorable = files.some((file) => file.restorable) ? "true" : "false";
|
|
4799
|
+
refreshCheckpointRestoreAvailability(run);
|
|
4800
|
+
body.innerHTML = (data.files && data.files.warning ? '<p class="hint">' + esc(data.files.warning) + "</p>" : "") +
|
|
4801
|
+
(files.length ? files.map((file) => '<article class="rd-checkpoint-row"><strong>' + esc(file.path) + '</strong><span class="chip">' + esc(file.status) + (file.restorable ? " · " + esc(t("可恢复")) : "") + '</span><pre>' + esc(file.diff || "") + "</pre></article>").join("") : '<span class="hint">' + esc(t("此运行没有文件快照")) + "</span>");
|
|
4802
|
+
box.dataset.runId = runId;
|
|
4803
|
+
box.classList.remove("needs-load");
|
|
4804
|
+
} catch (error) { body.innerHTML = '<span class="hint">' + esc(t("检查点读取失败:")) + esc(error.message) + "</span>"; }
|
|
4805
|
+
};
|
|
4806
|
+
|
|
4807
|
+
window.restoreRunCheckpoints = async function () {
|
|
4808
|
+
const runId = $("rd-checkpoints")?.dataset.runId;
|
|
4809
|
+
if (!runId || !await uiConfirm(t("恢复会覆盖检查点记录的文件,shell、MCP 和外部服务副作用不保证可回滚。确定继续?"), { ok: t("恢复文件"), danger: true })) return;
|
|
4810
|
+
try {
|
|
4811
|
+
const result = await api("/api/runs/" + encodeURIComponent(runId) + "/checkpoints/restore", {
|
|
4812
|
+
method: "POST", headers: Object.assign({ "Content-Type": "application/json" }, authHeaders()),
|
|
4813
|
+
body: JSON.stringify({ confirm: true }),
|
|
4814
|
+
});
|
|
4815
|
+
toast(t("已恢复 ") + (result.restored || 0) + t(" 个文件") + ((result.conflicts || []).length ? ";" + t("冲突 ") + result.conflicts.join(", ") : ""));
|
|
4816
|
+
await loadRunCheckpoints();
|
|
4817
|
+
} catch (error) { toast(t("恢复失败:") + error.message, true); }
|
|
4818
|
+
};
|
|
4819
|
+
|
|
4705
4820
|
function loadTaskRuns(key, latestSummary) {
|
|
4706
4821
|
const cached = _taskRunsCache.get(key);
|
|
4707
4822
|
if (cached) {
|
|
@@ -5501,6 +5616,18 @@ async function renderRunDetail() {
|
|
|
5501
5616
|
$("btn-share").classList.toggle("hidden", active); // 分享页:结束后可生成自包含 HTML
|
|
5502
5617
|
$("btn-talk").classList.toggle("hidden", !(run.task_id && !chatEngineIsDirect(run)));
|
|
5503
5618
|
const rcTask = ((S.state || {}).tasks || []).find((x) => x.id === run.task_id);
|
|
5619
|
+
if (rcTask) {
|
|
5620
|
+
renderProjectMemoryControls(rcTask.id);
|
|
5621
|
+
} else {
|
|
5622
|
+
const memoryBox = $("rd-project-memory");
|
|
5623
|
+
if (memoryBox) memoryBox.classList.add("hidden");
|
|
5624
|
+
}
|
|
5625
|
+
renderCheckpointControls(run.id, !!rcTask);
|
|
5626
|
+
refreshCheckpointRestoreAvailability(run);
|
|
5627
|
+
if (rcTask && S._checkpointRun !== run.id) {
|
|
5628
|
+
S._checkpointRun = run.id;
|
|
5629
|
+
loadRunCheckpoints(run.id);
|
|
5630
|
+
}
|
|
5504
5631
|
// 签名守卫(模型页/检查器同款):数据没变不重绘。详情区是全页最重的
|
|
5505
5632
|
// 渲染面,此前 SSE 推送+轮询每 8s 无差别整块重写,是「页面一闪一闪、
|
|
5506
5633
|
// 发布表单填一半被清空」的总根源(2026-09-30 用户实案)。运行中 ETA
|
|
@@ -6041,42 +6168,58 @@ function pbBlock(task, platform) {
|
|
|
6041
6168
|
if (run && run.status === "running") {
|
|
6042
6169
|
btns += '<span class="pb-book">' + esc(t("自动发布中 ") + run.done + "/" + run.total) + "</span>";
|
|
6043
6170
|
} else if (au.pending > 0) {
|
|
6044
|
-
|
|
6045
|
-
|
|
6171
|
+
// 门禁不再禁用按钮:点了走 pbApiWithQualityOverride 的复审/强制对话,
|
|
6172
|
+
// 死按钮只会让用户无路可走(2026-10-08 实案:guard false 全灰点不了)
|
|
6173
|
+
btns += ' <button class="ghost" ' + (busy ? "disabled" : "") +
|
|
6174
|
+
' title="' + esc(au.guard_ok ? t("按章号顺序逐章填稿(人工模式每点一次填一章,浏览器里提交后再点发下一章)") : t("护栏提示:") + (au.guard_reason || "")) + '"' +
|
|
6046
6175
|
' onclick="pbPublishAll(\'' + esc(task.id) + "', '" + platform + '\')">' +
|
|
6047
6176
|
t("发布全部待发") + t("(") + au.pending + t(")") + "</button>";
|
|
6048
6177
|
}
|
|
6049
|
-
// 批量选择:待发章号 chips
|
|
6050
|
-
//
|
|
6051
|
-
//
|
|
6052
|
-
const
|
|
6053
|
-
.map((x) => Number(x.chapter_no)
|
|
6054
|
-
|
|
6178
|
+
// 批量选择:待发章号 chips 按卷分组(卷计划来自服务端 items[].volume),
|
|
6179
|
+
// 「发布所选」人工模式 / 「直发所选」逐章自动提交(护栏照常生效)。
|
|
6180
|
+
// 分卷由服务端按卷计划自动选/建(volumes.py),界面上不用管卷。
|
|
6181
|
+
const its = (au.items && au.items.length ? au.items : [])
|
|
6182
|
+
.map((x) => ({ n: Number(x.chapter_no), v: x.volume || "" }))
|
|
6183
|
+
.filter((x) => x.n > 0);
|
|
6184
|
+
if (its.length && !run) {
|
|
6055
6185
|
const key = task.id + ":" + platform;
|
|
6056
|
-
const sig =
|
|
6186
|
+
const sig = its.map((x) => x.n).join(",");
|
|
6057
6187
|
S.pbSel = S.pbSel || {};
|
|
6058
6188
|
if (!S.pbSel[key] || S.pbSel[key].sig !== sig)
|
|
6059
|
-
S.pbSel[key] = { sig, sel: new Set(
|
|
6189
|
+
S.pbSel[key] = { sig, sel: new Set(its.map((x) => x.n)) };
|
|
6060
6190
|
const sel = S.pbSel[key].sel;
|
|
6191
|
+
// 连续段按卷名分组;无卷计划的整块平铺不显示卷头
|
|
6192
|
+
const groups = [];
|
|
6193
|
+
for (const x of its) {
|
|
6194
|
+
const g = groups[groups.length - 1];
|
|
6195
|
+
if (g && g.v === x.v) g.ns.push(x.n);
|
|
6196
|
+
else groups.push({ v: x.v, ns: [x.n] });
|
|
6197
|
+
}
|
|
6061
6198
|
let chips = "";
|
|
6062
|
-
for (const
|
|
6063
|
-
|
|
6064
|
-
'<
|
|
6065
|
-
|
|
6066
|
-
n + "
|
|
6199
|
+
for (const g of groups) {
|
|
6200
|
+
if (groups.length > 1 || g.v)
|
|
6201
|
+
chips += '<div class="pb-vol">' + esc(g.v || t("未分卷")) + "</div>";
|
|
6202
|
+
for (const n of g.ns)
|
|
6203
|
+
chips += '<label class="pb-chip' + (sel.has(n) ? " on" : "") + '">' +
|
|
6204
|
+
'<input type="checkbox" ' + (sel.has(n) ? "checked" : "") +
|
|
6205
|
+
' onchange="pbSelToggle(\'' + esc(task.id) + "','" + platform + "'," + n + ',this.checked)">' +
|
|
6206
|
+
n + "</label>";
|
|
6207
|
+
}
|
|
6067
6208
|
pendHtml += '<div class="pb-pend" data-key="' + esc(key) + '">' +
|
|
6068
6209
|
'<div class="pb-pend-head">' +
|
|
6069
|
-
'<span>' + esc(t("待发 ")) +
|
|
6210
|
+
'<span>' + esc(t("待发 ")) + its.length + esc(t(" 章(按卷计划自动分卷)")) + "</span>" +
|
|
6070
6211
|
'<a href="javascript:void(0)" onclick="pbSelAll(\'' + esc(task.id) + "','" + platform + "',true)" + '">' + esc(t("全选")) + "</a>" +
|
|
6071
6212
|
'<a href="javascript:void(0)" onclick="pbSelAll(\'' + esc(task.id) + "','" + platform + "',false)" + '">' + esc(t("清空")) + "</a>" +
|
|
6072
6213
|
"</div>" +
|
|
6073
6214
|
'<div class="pb-pend-chips">' + chips + "</div>" +
|
|
6074
6215
|
'<div class="pb-pend-btns">' +
|
|
6075
|
-
'<button class="ghost" ' + (busy || !
|
|
6216
|
+
'<button class="ghost" ' + (busy || !sel.size ? "disabled" : "") +
|
|
6217
|
+
' title="' + esc(au.guard_ok ? "" : t("护栏提示:") + (au.guard_reason || "")) + '"' +
|
|
6076
6218
|
' onclick="pbPublishSelected(\'' + esc(task.id) + "','" + platform + '\',false)">' +
|
|
6077
6219
|
t("发布所选") + t("(") + sel.size + t(")") + "</button>" +
|
|
6078
|
-
'<button class="ghost" ' + (busy || !
|
|
6079
|
-
' title="' + esc(t("所选章节逐章自动提交(无人值守),护栏每日上限/连败退避照常生效")
|
|
6220
|
+
'<button class="ghost" ' + (busy || !sel.size ? "disabled" : "") +
|
|
6221
|
+
' title="' + esc(t("所选章节逐章自动提交(无人值守),护栏每日上限/连败退避照常生效") +
|
|
6222
|
+
(au.guard_ok ? "" : ";" + t("护栏提示:") + (au.guard_reason || ""))) + '"' +
|
|
6080
6223
|
' onclick="pbPublishSelected(\'' + esc(task.id) + "','" + platform + '\',true)">' +
|
|
6081
6224
|
t("直发所选") + t("(") + sel.size + t(")") + "</button>" +
|
|
6082
6225
|
"</div></div>";
|
|
@@ -6358,12 +6501,36 @@ window.pbConfirmChapter = async function (taskId, platform, chapterNo) {
|
|
|
6358
6501
|
S._pbSig = ""; pbKick();
|
|
6359
6502
|
};
|
|
6360
6503
|
|
|
6504
|
+
function pbSelCountSync(key) {
|
|
6505
|
+
// 就地对账:按钮计数/禁用态与全部 chip 勾选态向状态层看齐——重绘链一旦
|
|
6506
|
+
// 断掉(poll 失败/详情上下文切换),勾选与计数也不许脱节(2026-10-08 实案
|
|
6507
|
+
// 「勾了 18 计数 48」即重绘未跟上)。
|
|
6508
|
+
const box = document.querySelector('.pb-pend[data-key="' +
|
|
6509
|
+
String(key).replace(/"/g, """) + '"]');
|
|
6510
|
+
const ent = (S.pbSel || {})[key];
|
|
6511
|
+
if (!box || !ent) return;
|
|
6512
|
+
for (const b of box.querySelectorAll("button")) {
|
|
6513
|
+
const t = b.textContent || "";
|
|
6514
|
+
if (/(\d+)$/.test(t)) b.textContent = t.replace(/(\d+)$/, "(" + ent.sel.size + ")");
|
|
6515
|
+
b.disabled = ent.sel.size === 0;
|
|
6516
|
+
}
|
|
6517
|
+
for (const c of box.querySelectorAll(".pb-chip")) {
|
|
6518
|
+
const inp = c.querySelector("input");
|
|
6519
|
+
const num = Number((c.textContent || "").trim());
|
|
6520
|
+
if (!inp || !num) continue;
|
|
6521
|
+
const on = ent.sel.has(num);
|
|
6522
|
+
if (inp.checked !== on) inp.checked = on;
|
|
6523
|
+
c.classList.toggle("on", on);
|
|
6524
|
+
}
|
|
6525
|
+
}
|
|
6526
|
+
|
|
6361
6527
|
window.pbSelToggle = function (taskId, platform, no, on) {
|
|
6362
6528
|
const key = taskId + ":" + platform;
|
|
6363
6529
|
const ent = (S.pbSel || {})[key];
|
|
6364
6530
|
if (!ent) return;
|
|
6365
6531
|
if (on) ent.sel.add(Number(no)); else ent.sel.delete(Number(no));
|
|
6366
|
-
|
|
6532
|
+
pbSelCountSync(key); // 先就地同步,再触发整块重绘
|
|
6533
|
+
S._pbSig = ""; pbKick();
|
|
6367
6534
|
};
|
|
6368
6535
|
|
|
6369
6536
|
window.pbSelAll = function (taskId, platform, all) {
|
|
@@ -6372,6 +6539,7 @@ window.pbSelAll = function (taskId, platform, all) {
|
|
|
6372
6539
|
if (!ent) return;
|
|
6373
6540
|
if (all) for (const n of ent.sig.split(",")) ent.sel.add(Number(n));
|
|
6374
6541
|
else ent.sel.clear();
|
|
6542
|
+
pbSelCountSync(key);
|
|
6375
6543
|
S._pbSig = ""; pbKick();
|
|
6376
6544
|
};
|
|
6377
6545
|
|
|
@@ -8337,26 +8505,17 @@ function hiveThinkLine(lines) {
|
|
|
8337
8505
|
return "";
|
|
8338
8506
|
}
|
|
8339
8507
|
|
|
8340
|
-
|
|
8341
|
-
|
|
8342
|
-
|
|
8343
|
-
|
|
8344
|
-
|
|
8345
|
-
|
|
8346
|
-
|
|
8347
|
-
|
|
8348
|
-
|
|
8508
|
+
/* —— 3D 场景(WebGL 蜂巢塔)控制层:引擎在 hive3d.js,这里只管模式切换/
|
|
8509
|
+
* localStorage 持久化/WebGL 不可用降级,以及把 renderHive 的泳道数据翻译给场景。 —— */
|
|
8510
|
+
let hiveScene = null; // Hive3D 场景实例(懒创建)
|
|
8511
|
+
let hiveSceneDead = false; // WebGL 不可用/上下文丢失——本次页面周期内不再尝试
|
|
8512
|
+
let hiveMode = "3d"; // 当前视图模式(orch.hiveView)
|
|
8513
|
+
let hiveRunId = ""; // 最近一次 renderHive 的 run.id(尾巴/思考缓存键前缀)
|
|
8514
|
+
const hiveLiveMeta = {}; // rel -> { started_at }(3D 芯片秒表用)
|
|
8515
|
+
let hiveDialogPoll = null;
|
|
8516
|
+
let hiveDialogKey = "";
|
|
8349
8517
|
|
|
8350
|
-
function
|
|
8351
|
-
const scene = $("rd-hive-cells");
|
|
8352
|
-
const viewport = $("rd-hive-viewport");
|
|
8353
|
-
const tools = document.querySelector(".hive-scene-tools");
|
|
8354
|
-
if (!scene || !viewport) return;
|
|
8355
|
-
const is3d = mode !== "2d";
|
|
8356
|
-
scene.classList.toggle("hive-3d", is3d);
|
|
8357
|
-
scene.classList.toggle("hive-2d", !is3d);
|
|
8358
|
-
viewport.classList.toggle("hive-2d", !is3d);
|
|
8359
|
-
if (tools) tools.classList.toggle("hive-2d", !is3d);
|
|
8518
|
+
function setHiveSceneButtons(is3d) {
|
|
8360
8519
|
document.querySelectorAll("[data-hive-view]").forEach((button) => {
|
|
8361
8520
|
const active = button.dataset.hiveView === (is3d ? "3d" : "2d");
|
|
8362
8521
|
button.classList.toggle("active", active);
|
|
@@ -8364,66 +8523,180 @@ function setHiveSceneMode(mode) {
|
|
|
8364
8523
|
});
|
|
8365
8524
|
}
|
|
8366
8525
|
|
|
8526
|
+
function ensureHiveScene() {
|
|
8527
|
+
if (hiveScene || hiveSceneDead) return hiveScene;
|
|
8528
|
+
const canvas = $("rd-hive-gl"), overlay = $("rd-hive-overlay");
|
|
8529
|
+
if (!canvas || !overlay || !window.Hive3D) { hiveSceneDead = true; return null; }
|
|
8530
|
+
hiveScene = window.Hive3D.create({
|
|
8531
|
+
canvas, overlay,
|
|
8532
|
+
onCellActivate(rid, rel, cell) { openHiveMonitorLog(rid, rel, cell); },
|
|
8533
|
+
/* 悬停气泡与运行中芯片的实时内容:尾巴/思考来自 hiveTick 的缓存,秒表现算 */
|
|
8534
|
+
cellRefresh(rel) {
|
|
8535
|
+
const key = hiveRunId + "|" + rel;
|
|
8536
|
+
const meta = hiveLiveMeta[rel] || {};
|
|
8537
|
+
return {
|
|
8538
|
+
tail: hiveTails[key] || "",
|
|
8539
|
+
think: hiveThink[key] || "",
|
|
8540
|
+
elapsed: meta.started_at ? hiveElapsed(meta.started_at) : "",
|
|
8541
|
+
};
|
|
8542
|
+
},
|
|
8543
|
+
onFatal() { // 上下文丢失等不可恢复故障:退回 2D 并禁用 3D 入口
|
|
8544
|
+
hiveSceneDead = true;
|
|
8545
|
+
hiveScene = null;
|
|
8546
|
+
setHiveSceneMode("2d", { silent: true });
|
|
8547
|
+
toast(t("3D 场景不可用,已切换 2D 列表"), true);
|
|
8548
|
+
},
|
|
8549
|
+
});
|
|
8550
|
+
if (!hiveScene) hiveSceneDead = true;
|
|
8551
|
+
return hiveScene;
|
|
8552
|
+
}
|
|
8553
|
+
|
|
8554
|
+
async function openHiveMonitorLog(runId, rel, cell) {
|
|
8555
|
+
if (!rel) { toast(t("该步骤无日志"), true); return; }
|
|
8556
|
+
const dialog = $("hive-log-dialog"), title = $("hive-log-title"), meta = $("hive-log-meta"), pre = $("hive-log-text"), error = $("hive-log-error");
|
|
8557
|
+
if (!dialog || !title || !meta || !pre) { hiveOpenLog(runId, rel); return; }
|
|
8558
|
+
if (hiveDialogPoll) { clearInterval(hiveDialogPoll); hiveDialogPoll = null; }
|
|
8559
|
+
hiveDialogKey = runId + "|" + rel;
|
|
8560
|
+
title.textContent = cell && cell.role ? t(cell.role) : rdLogStepLabel(runId, rel);
|
|
8561
|
+
const status = cell && cell.status ? t({ running: "运行中", done: "完成", queued: "排队中", failed: "失败", timeout: "超时", cancelled: "已取消" }[cell.status] || cell.status) : "";
|
|
8562
|
+
meta.textContent = [status, cell && cell.displayAgent, cell && (cell.displayElapsed || cell.started_at)].filter(Boolean).join(" · ");
|
|
8563
|
+
pre.textContent = t("加载日志中…");
|
|
8564
|
+
if (error) error.textContent = "";
|
|
8565
|
+
if (typeof dialog.showModal === "function") dialog.showModal(); else dialog.setAttribute("open", "");
|
|
8566
|
+
const load = async () => {
|
|
8567
|
+
try {
|
|
8568
|
+
const r = await api("/api/runs/" + encodeURIComponent(runId) + "/log?step=" + encodeURIComponent(rel) + "&pretty=1");
|
|
8569
|
+
if (hiveDialogKey !== runId + "|" + rel) return;
|
|
8570
|
+
pre.textContent = r.log || (r.step_status === "running" ? t("(等待输出…)") : t("(无输出)"));
|
|
8571
|
+
pre.scrollTop = pre.scrollHeight;
|
|
8572
|
+
if (r.step_status !== "running" && hiveDialogPoll) { clearInterval(hiveDialogPoll); hiveDialogPoll = null; }
|
|
8573
|
+
} catch (e) {
|
|
8574
|
+
if (error) error.textContent = t("日志读取失败:") + (e && e.message ? e.message : "");
|
|
8575
|
+
}
|
|
8576
|
+
};
|
|
8577
|
+
await load();
|
|
8578
|
+
if (cell && cell.status === "running") hiveDialogPoll = setInterval(load, 2500);
|
|
8579
|
+
}
|
|
8580
|
+
|
|
8581
|
+
function closeHiveMonitorLog() {
|
|
8582
|
+
hiveDialogKey = "";
|
|
8583
|
+
if (hiveDialogPoll) { clearInterval(hiveDialogPoll); hiveDialogPoll = null; }
|
|
8584
|
+
const dialog = $("hive-log-dialog");
|
|
8585
|
+
if (dialog && dialog.open) dialog.close();
|
|
8586
|
+
}
|
|
8587
|
+
|
|
8588
|
+
function setHiveSceneMode(mode, opts) {
|
|
8589
|
+
const viewport = $("rd-hive-viewport");
|
|
8590
|
+
if (!viewport) return;
|
|
8591
|
+
let is3d = mode !== "2d";
|
|
8592
|
+
if (is3d) {
|
|
8593
|
+
const sc = ensureHiveScene();
|
|
8594
|
+
if (!sc) {
|
|
8595
|
+
is3d = false;
|
|
8596
|
+
if (!(opts && opts.silent)) toast(t("当前环境不支持 WebGL,已切换 2D 列表"), true);
|
|
8597
|
+
}
|
|
8598
|
+
}
|
|
8599
|
+
hiveMode = is3d ? "3d" : "2d";
|
|
8600
|
+
if (hiveScene) hiveScene.setActive(is3d);
|
|
8601
|
+
viewport.classList.toggle("hive-mode-3d", is3d);
|
|
8602
|
+
viewport.classList.toggle("hive-mode-2d", !is3d);
|
|
8603
|
+
const tools = document.querySelector(".hive-scene-tools");
|
|
8604
|
+
if (tools) tools.classList.toggle("hive-2d", !is3d);
|
|
8605
|
+
setHiveSceneButtons(is3d);
|
|
8606
|
+
try { localStorage.setItem("orch.hiveView", hiveMode); } catch (e) { /* 隐私模式等 */ }
|
|
8607
|
+
}
|
|
8608
|
+
|
|
8367
8609
|
function setupHiveSceneControls() {
|
|
8368
8610
|
const viewport = $("rd-hive-viewport");
|
|
8369
|
-
|
|
8370
|
-
if (!viewport || !scene || viewport.dataset.controlsReady) return;
|
|
8611
|
+
if (!viewport || viewport.dataset.controlsReady) return;
|
|
8371
8612
|
viewport.dataset.controlsReady = "true";
|
|
8372
|
-
|
|
8373
|
-
|
|
8613
|
+
const hiveDialog = $("hive-log-dialog");
|
|
8614
|
+
const hiveClose = $("hive-log-close");
|
|
8615
|
+
if (hiveClose) hiveClose.addEventListener("click", closeHiveMonitorLog);
|
|
8616
|
+
if (hiveDialog) hiveDialog.addEventListener("close", () => {
|
|
8617
|
+
hiveDialogKey = "";
|
|
8618
|
+
if (hiveDialogPoll) { clearInterval(hiveDialogPoll); hiveDialogPoll = null; }
|
|
8619
|
+
});
|
|
8374
8620
|
document.querySelectorAll("[data-hive-view]").forEach((button) => {
|
|
8375
8621
|
button.addEventListener("click", () => setHiveSceneMode(button.dataset.hiveView));
|
|
8376
8622
|
});
|
|
8377
8623
|
document.querySelectorAll("[data-hive-scene-action]").forEach((button) => {
|
|
8378
8624
|
button.addEventListener("click", () => {
|
|
8379
|
-
if (
|
|
8380
|
-
|
|
8381
|
-
|
|
8382
|
-
hiveSceneTransform.zoom = 1;
|
|
8383
|
-
} else {
|
|
8384
|
-
hiveSceneTransform.zoom = Math.max(.65, Math.min(1.55,
|
|
8385
|
-
hiveSceneTransform.zoom + (button.dataset.hiveSceneAction === "zoom-in" ? .1 : -.1)));
|
|
8386
|
-
}
|
|
8387
|
-
updateHiveSceneTransform();
|
|
8625
|
+
if (hiveMode !== "3d" || !hiveScene) return;
|
|
8626
|
+
if (button.dataset.hiveSceneAction === "reset") hiveScene.resetView();
|
|
8627
|
+
else hiveScene.zoomAt(button.dataset.hiveSceneAction === "zoom-in" ? 1 / 1.18 : 1.18);
|
|
8388
8628
|
});
|
|
8389
8629
|
});
|
|
8390
|
-
|
|
8391
|
-
|
|
8392
|
-
|
|
8393
|
-
|
|
8394
|
-
|
|
8395
|
-
|
|
8396
|
-
|
|
8397
|
-
|
|
8398
|
-
|
|
8399
|
-
|
|
8400
|
-
|
|
8401
|
-
|
|
8402
|
-
|
|
8403
|
-
|
|
8630
|
+
let saved = "3d";
|
|
8631
|
+
try { saved = localStorage.getItem("orch.hiveView") || "3d"; } catch (e) { /* ignore */ }
|
|
8632
|
+
setHiveSceneMode(saved === "2d" ? "2d" : "3d", { silent: true });
|
|
8633
|
+
}
|
|
8634
|
+
|
|
8635
|
+
/* renderHive → 场景数据翻译:泳道=蜂巢塔的一层,格子按状态映射高度/颜色。
|
|
8636
|
+
* 气泡卡 HTML 在这里用 esc() 拼好(场景端 innerHTML 直用,自己不转义);
|
|
8637
|
+
* 运行中格子的尾巴/思考行留空,由场景每 2Hz 经 cellRefresh 回填。 */
|
|
8638
|
+
function hiveSceneSync(run, lanes, byStage) {
|
|
8639
|
+
hiveRunId = run.id;
|
|
8640
|
+
for (const k of Object.keys(hiveLiveMeta)) delete hiveLiveMeta[k];
|
|
8641
|
+
const model = { runId: run.id, lanes: [] };
|
|
8642
|
+
lanes.forEach((stage, i) => {
|
|
8643
|
+
const list = byStage[stage];
|
|
8644
|
+
const hasRun = list.some((s) => s.status === "running");
|
|
8645
|
+
const settled = list.filter((s) => !["running", "queued"].includes(s.status)).length;
|
|
8646
|
+
model.lanes.push({
|
|
8647
|
+
name: stage, count: list.length, settled, active: hasRun,
|
|
8648
|
+
/* 工位悬停气泡(阶段级):进度 + 最近落定步骤 */
|
|
8649
|
+
tip:
|
|
8650
|
+
'<b class="hg-tip-role">' + esc(stage) + "</b>" +
|
|
8651
|
+
'<i class="hg-tip-who">' + esc(settled + " / " + list.length) +
|
|
8652
|
+
(hasRun ? " · " + esc(t("进行中")) : "") + "</i>" +
|
|
8653
|
+
'<p class="hg-tip-tail">' +
|
|
8654
|
+
esc(list.filter((s) => !["running", "queued"].includes(s.status)).slice(-1).map((s) => s.role || "").join("")) +
|
|
8655
|
+
"</p>" +
|
|
8656
|
+
'<u class="hg-tip-meta">' + esc(t("点击工位看该阶段最新日志")) + "</u>",
|
|
8657
|
+
cells: list.slice(-8).map((s) => {
|
|
8658
|
+
const st = s.status === "queued" ? "queued"
|
|
8659
|
+
: s.status === "running" ? "running"
|
|
8660
|
+
: s.status === "failed" ? "failed"
|
|
8661
|
+
: s.status === "cancelled" ? "cancelled"
|
|
8662
|
+
: s.status === "timeout" ? "timeout" : "done";
|
|
8663
|
+
const who = s.agent_label || s.agent || "";
|
|
8664
|
+
const mdl = String(s.model || "").trim();
|
|
8665
|
+
const whoShow = who + (mdl ? " · " + mdl : "");
|
|
8666
|
+
const rel = s.log || "";
|
|
8667
|
+
if (st === "running") hiveLiveMeta[rel] = { started_at: s.started_at || "" };
|
|
8668
|
+
const concl = String(s.summary || "").replace(/\s+/g, " ").trim();
|
|
8669
|
+
const elapsed = s.duration_s != null ? s.duration_s + "s" : hiveElapsed(s.started_at);
|
|
8670
|
+
const stWord = { running: t("运行中"), done: t("完成"), failed: t("失败"),
|
|
8671
|
+
timeout: t("超时"), cancelled: t("已取消"), queued: t("排队中") }[st] || s.status;
|
|
8672
|
+
const metaBits = [stWord, elapsed, rel ? t("点击格子看日志") : ""].filter(Boolean).join(" · ");
|
|
8673
|
+
const tip =
|
|
8674
|
+
'<b class="hg-tip-role">' + esc(s.role || "") + "</b>" +
|
|
8675
|
+
(whoShow ? '<i class="hg-tip-who">' + esc(whoShow) + "</i>" : "") +
|
|
8676
|
+
'<p class="hg-tip-tail">' + (st === "running" ? "" : esc(concl)) + "</p>" +
|
|
8677
|
+
'<em class="hg-tip-think"></em>' +
|
|
8678
|
+
'<u class="hg-tip-meta">' + esc(metaBits) + "</u>";
|
|
8679
|
+
return {
|
|
8680
|
+
rel, role: s.role || "", status: st, tip,
|
|
8681
|
+
displayTail: st === "running" ? "" : concl,
|
|
8682
|
+
displayStatus: stWord,
|
|
8683
|
+
displayElapsed: elapsed,
|
|
8684
|
+
displayAgent: whoShow,
|
|
8685
|
+
};
|
|
8686
|
+
}),
|
|
8687
|
+
});
|
|
8404
8688
|
});
|
|
8405
|
-
const
|
|
8406
|
-
|
|
8407
|
-
|
|
8408
|
-
|
|
8409
|
-
};
|
|
8410
|
-
viewport.addEventListener("pointerup", endDrag);
|
|
8411
|
-
viewport.addEventListener("pointercancel", endDrag);
|
|
8412
|
-
viewport.addEventListener("lostpointercapture", endDrag);
|
|
8413
|
-
viewport.addEventListener("wheel", (event) => {
|
|
8414
|
-
if (!scene.classList.contains("hive-3d")) return;
|
|
8415
|
-
event.preventDefault();
|
|
8416
|
-
hiveSceneTransform.zoom = Math.max(.65, Math.min(1.55,
|
|
8417
|
-
hiveSceneTransform.zoom * (event.deltaY < 0 ? 1.08 : .92)));
|
|
8418
|
-
updateHiveSceneTransform();
|
|
8419
|
-
}, { passive: false });
|
|
8689
|
+
const sc = ensureHiveScene();
|
|
8690
|
+
if (!sc) return;
|
|
8691
|
+
sc.sync(model); // 2D 模式也同步:切回 3D 时数据即 ready(画布未激活零 GPU 开销)
|
|
8692
|
+
sc.setActive(hiveMode === "3d");
|
|
8420
8693
|
}
|
|
8421
8694
|
|
|
8422
8695
|
window.renderHive = function (run) {
|
|
8423
8696
|
const box = $("rd-hive");
|
|
8424
8697
|
if (!box) return;
|
|
8425
8698
|
const steps = (run && run.steps) || [];
|
|
8426
|
-
if (!run || !steps.length) { box.classList.add("hidden"); stopHiveTick(); return; }
|
|
8699
|
+
if (!run || !steps.length) { box.classList.add("hidden"); stopHiveTick(); if (hiveScene) hiveScene.setActive(false); return; }
|
|
8427
8700
|
box.classList.remove("hidden");
|
|
8428
8701
|
setupHiveSceneControls();
|
|
8429
8702
|
// 按步骤首次出现顺序分泳道(流水线天然有序)
|
|
@@ -8551,6 +8824,7 @@ window.renderHive = function (run) {
|
|
|
8551
8824
|
if (cellsBox._hiveEnterT) clearTimeout(cellsBox._hiveEnterT);
|
|
8552
8825
|
cellsBox._hiveEnterT = setTimeout(() => cellsBox.classList.remove("hive-enter"), 1400);
|
|
8553
8826
|
}
|
|
8827
|
+
hiveSceneSync(run, lanes, byStage); // WebGL 蜂巢塔吃同一份泳道数据(内部自管激活)
|
|
8554
8828
|
const live = running.length && (run.status === "running" || run.status === "queued");
|
|
8555
8829
|
if (hiveClock) { clearInterval(hiveClock); hiveClock = null; }
|
|
8556
8830
|
if (live) {
|
|
Binary file
|