codebee 0.1.6 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/README.md +431 -421
- package/app/core/automation.py +30 -5
- package/app/core/catalog.py +23 -0
- package/app/core/health.py +7 -50
- package/app/core/manager.py +80 -11
- package/app/core/modelhub.py +2883 -2896
- package/app/core/pipeline.py +9 -15
- package/app/core/remote.py +310 -303
- package/app/core/runner.py +26 -3
- package/app/core/selfupdate.py +47 -16
- package/app/core/store.py +138 -47
- package/app/core/usage.py +51 -1
- package/app/main.py +219 -23
- package/app/ui/app.js +519 -156
- package/app/ui/i18n.js +30 -1
- package/app/ui/index.html +57 -45
- package/app/ui/style.css +1056 -2
- package/package.json +1 -1
package/app/ui/app.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"use strict";
|
|
3
3
|
|
|
4
4
|
const $ = (id) => document.getElementById(id);
|
|
5
|
-
const S = { state: null, catalog: null, catSig: "", providers: null, bindings: null, modelsSig: "", bindSig: "", tab: "tasks", detailRunId: null, pollTimer: null, showArchived: false, /* 会话内开关:每次加载默认隐藏已归档、图标不选中(不持久化,见 btn-side-arch) */ selProvs: {}, selModels: {}, selRuns: {}, bindSel: {}, catalogChecking: false, updateCheckAt: 0, control: null, sseLive: false, es: null, flows: null, orch: null, skills: null, orchSig: "", settings: null, sessionAgents: new Set(), atts: [], gitInfo: null, gitWb: null, gitWbKey: "", gitWbAt: 0, gitWbBusy: false, inspKey: null, inspData: null, inspSig: "", inspAt: 0, inspTab: "git", inspAutoSig: "", rdTab: null, rdTabSig: "", rdTabPin: false, _rdCtx: {} };
|
|
5
|
+
const S = { state: null, catalog: null, catSig: "", providers: null, bindings: null, modelsSig: "", bindSig: "", tab: "tasks", detailRunId: null, pollTimer: null, showArchived: false, /* 会话内开关:每次加载默认隐藏已归档、图标不选中(不持久化,见 btn-side-arch) */ selProvs: {}, selModels: {}, selRuns: {}, bindSel: {}, catalogChecking: false, updateCheckAt: 0, control: null, sseLive: false, es: null, flows: null, orch: null, skills: null, orchSig: "", settings: null, sessionAgents: new Set(), atts: [], gitInfo: null, gitWb: null, gitWbKey: "", gitWbAt: 0, gitWbBusy: false, creatingTask: false, inspKey: null, inspData: null, inspSig: "", inspAt: 0, inspTab: "git", inspAutoSig: "", rdTab: null, rdTabSig: "", rdTabPin: false, _rdCtx: {} };
|
|
6
6
|
|
|
7
7
|
/* ---------------------------------------------------------- 任务类型(流程) */
|
|
8
8
|
async function loadFlows() {
|
|
@@ -97,10 +97,17 @@ function onTypeChange() {
|
|
|
97
97
|
const engine = flow ? flow.engine : "code";
|
|
98
98
|
const isReview = engine === "review";
|
|
99
99
|
const isDirect = engine === "direct";
|
|
100
|
-
const codeOnly = $("f-code-only"), reviewOnly = $("f-review-only");
|
|
100
|
+
const codeOnly = $("f-code-only"), reviewOnly = $("f-review-only");
|
|
101
101
|
// 直连:验证命令与评审参数都不适用,两块一起收起(目标+附件即全部输入)
|
|
102
|
-
if (codeOnly) codeOnly.classList.toggle("hidden", isReview || isDirect);
|
|
103
|
-
if (reviewOnly) reviewOnly.classList.toggle("hidden", !isReview);
|
|
102
|
+
if (codeOnly) codeOnly.classList.toggle("hidden", isReview || isDirect);
|
|
103
|
+
if (reviewOnly) reviewOnly.classList.toggle("hidden", !isReview);
|
|
104
|
+
const bibleCreate = $("bible-create-field");
|
|
105
|
+
const isSerialReview = isReview && !!(flow && flow.serial);
|
|
106
|
+
if (bibleCreate) bibleCreate.classList.toggle("hidden", !isSerialReview);
|
|
107
|
+
const clearSerialFields = () => ["f-chapters", "f-words-per-ch", "f-variants", "f-bible"].forEach((id) => {
|
|
108
|
+
const el = $(id);
|
|
109
|
+
if (el) el.value = "";
|
|
110
|
+
});
|
|
104
111
|
const goal = $("f-goal");
|
|
105
112
|
if (goal && flow && flow.goal_hint) goal.placeholder = t(flow.goal_hint);
|
|
106
113
|
if (isReview && flow) {
|
|
@@ -110,12 +117,16 @@ function onTypeChange() {
|
|
|
110
117
|
const saved = ($("f-rubric").value || "").trim();
|
|
111
118
|
if (!saved && flow.rubric) $("f-rubric").value = flow.rubric.join(", ");
|
|
112
119
|
// 连载参数预填(用户可改/可清空 = 单稿件模式)
|
|
113
|
-
if (flow.serial) {
|
|
114
|
-
if (!$("f-chapters").value) $("f-chapters").value = flow.serial.chapters || "";
|
|
115
|
-
if (!$("f-words-per-ch").value) $("f-words-per-ch").value = flow.serial.words_per_chapter || "";
|
|
116
|
-
}
|
|
117
|
-
}
|
|
120
|
+
if (flow.serial) {
|
|
121
|
+
if (!$("f-chapters").value) $("f-chapters").value = flow.serial.chapters || "";
|
|
122
|
+
if (!$("f-words-per-ch").value) $("f-words-per-ch").value = flow.serial.words_per_chapter || "";
|
|
123
|
+
} else clearSerialFields();
|
|
124
|
+
} else {
|
|
125
|
+
// 隐藏控件仍保留 DOM 值;换流程时清理,避免章节/圣经状态污染下一次提交。
|
|
126
|
+
clearSerialFields();
|
|
127
|
+
}
|
|
118
128
|
renderImplSelects();
|
|
129
|
+
refreshEstimate();
|
|
119
130
|
}
|
|
120
131
|
|
|
121
132
|
/* 快捷类型 chips(Composer 框下方):内置流程一键选定,选中态跟 #f-type 走 */
|
|
@@ -149,6 +160,28 @@ function cmpGreeting() {
|
|
|
149
160
|
: "晚上好呀,有什么想让我帮忙的吗");
|
|
150
161
|
}
|
|
151
162
|
|
|
163
|
+
/* 开跑前成本预估(借鉴 omnigent 的 pre-run estimate):读台账同类任务给量级参考。
|
|
164
|
+
* 无样本/请求失败都保持隐藏;快速切类型用序号丢弃过期响应。 */
|
|
165
|
+
let _estSeq = 0;
|
|
166
|
+
async function refreshEstimate() {
|
|
167
|
+
const el = $("cmp-estimate");
|
|
168
|
+
const flowId = ($("f-type") || {}).value || "";
|
|
169
|
+
if (!el) return;
|
|
170
|
+
const seq = ++_estSeq;
|
|
171
|
+
try {
|
|
172
|
+
const d = await api("/api/usage/estimate?type=" + encodeURIComponent(flowId) + "&days=90");
|
|
173
|
+
if (seq !== _estSeq) return;
|
|
174
|
+
if (!d || !(d.samples > 0)) { el.classList.add("hidden"); el.textContent = ""; return; }
|
|
175
|
+
const tk = "≈" + fmtTok(d.median_tokens) + " tokens";
|
|
176
|
+
const tail = "(" + t("近{0}天 · {1}次同类", d.days || 90, d.samples) + ")";
|
|
177
|
+
const cost = (d.median_cost_usd || 0) > 0 ? " · ≈$" + (d.median_cost_usd).toFixed(2) : "";
|
|
178
|
+
el.textContent = t("预估:≈{0}{1}", tk, cost) + tail;
|
|
179
|
+
el.classList.remove("hidden");
|
|
180
|
+
} catch (e) {
|
|
181
|
+
if (seq === _estSeq) { el.classList.add("hidden"); el.textContent = ""; }
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
152
185
|
/* ---------------------------------------------------------- 本机身份与鉴权 */
|
|
153
186
|
function clientId() {
|
|
154
187
|
let id = localStorage.getItem("orch.client");
|
|
@@ -1770,18 +1803,39 @@ function renderImplSelects() {
|
|
|
1770
1803
|
}
|
|
1771
1804
|
}
|
|
1772
1805
|
|
|
1773
|
-
async function createTask() {
|
|
1774
|
-
|
|
1806
|
+
async function createTask() {
|
|
1807
|
+
if (S.creatingTask) return;
|
|
1808
|
+
S.creatingTask = true;
|
|
1809
|
+
const submit = $("btn-create");
|
|
1810
|
+
if (submit) {
|
|
1811
|
+
submit.disabled = true;
|
|
1812
|
+
submit.setAttribute("aria-busy", "true");
|
|
1813
|
+
}
|
|
1814
|
+
const resetSubmit = () => {
|
|
1815
|
+
S.creatingTask = false;
|
|
1816
|
+
if (submit) {
|
|
1817
|
+
submit.disabled = false;
|
|
1818
|
+
submit.removeAttribute("aria-busy");
|
|
1819
|
+
}
|
|
1820
|
+
};
|
|
1821
|
+
const msg = $("create-msg");
|
|
1775
1822
|
msg.className = "msg"; msg.textContent = t("提交中…");
|
|
1776
1823
|
const payload = {
|
|
1777
1824
|
type: $("f-type").value,
|
|
1778
1825
|
mode: $("f-mode").value,
|
|
1779
1826
|
title: $("f-title").value.trim(),
|
|
1780
1827
|
goal: $("f-goal").value.trim(),
|
|
1781
|
-
context: $("f-context").value.trim(),
|
|
1782
|
-
workdir: $("f-workdir").value.trim(),
|
|
1783
|
-
};
|
|
1784
|
-
if (!payload.
|
|
1828
|
+
context: $("f-context").value.trim(),
|
|
1829
|
+
workdir: $("f-workdir").value.trim(),
|
|
1830
|
+
};
|
|
1831
|
+
if (!payload.goal) {
|
|
1832
|
+
msg.className = "msg err";
|
|
1833
|
+
msg.textContent = t("请先填写目标");
|
|
1834
|
+
$("f-goal").focus();
|
|
1835
|
+
resetSubmit();
|
|
1836
|
+
return;
|
|
1837
|
+
}
|
|
1838
|
+
if (!payload.workdir) delete payload.workdir; // 留空 → 服务端用「默认保存路径」(编排设置可改)
|
|
1785
1839
|
else unhideSideDir(payload.workdir); // 在已移除的目录新建任务 → 自动恢复显示
|
|
1786
1840
|
if (payload.mode === "manual") payload.implementer = $("f-impl").value;
|
|
1787
1841
|
// 附件与代码版本:任务创建时服务端把待提交附件移入 _attachments/ 并注入上下文
|
|
@@ -1799,7 +1853,8 @@ async function createTask() {
|
|
|
1799
1853
|
payload.resume = { agent: resumeAgent, session: sid,
|
|
1800
1854
|
preview: opt ? opt.textContent : "", project: (hit && hit.project) || "" };
|
|
1801
1855
|
}
|
|
1802
|
-
const
|
|
1856
|
+
const flow = flowById(payload.type) || {};
|
|
1857
|
+
const _eng = flow.engine;
|
|
1803
1858
|
if (_eng === "code") {
|
|
1804
1859
|
payload.verify_command = $("f-verify").value.trim();
|
|
1805
1860
|
} else if (_eng === "direct") {
|
|
@@ -1811,11 +1866,18 @@ async function createTask() {
|
|
|
1811
1866
|
const rubric = $("f-rubric").value.trim();
|
|
1812
1867
|
if (rubric) payload.rubric = rubric.split(/[,,、]/).map((s) => s.trim()).filter(Boolean);
|
|
1813
1868
|
const ch = parseInt($("f-chapters").value, 10);
|
|
1814
|
-
if (ch >= 2)
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1869
|
+
if (ch >= 2) {
|
|
1870
|
+
payload.serial = {
|
|
1871
|
+
chapters: ch,
|
|
1872
|
+
words_per_chapter: parseInt($("f-words-per-ch").value, 10) || 2500,
|
|
1873
|
+
variants: Math.max(1, Math.min(3, parseInt($("f-variants").value, 10) || 1)),
|
|
1874
|
+
};
|
|
1875
|
+
} else if (flow.serial) {
|
|
1876
|
+
// 显式 null 覆盖 serial_novel 的流程默认,空章节就是单稿件。
|
|
1877
|
+
payload.serial = null;
|
|
1878
|
+
}
|
|
1879
|
+
const initialBible = ($("f-bible") || {}).value ? $("f-bible").value.trim() : "";
|
|
1880
|
+
if (initialBible && flow.serial && payload.serial) payload.story_bible = initialBible;
|
|
1819
1881
|
const critics = Array.from($("f-critics").querySelectorAll("input:checked")).map((i) => i.value);
|
|
1820
1882
|
if (critics.length) payload.critics = critics;
|
|
1821
1883
|
}
|
|
@@ -1827,11 +1889,14 @@ async function createTask() {
|
|
|
1827
1889
|
try { await refreshState(); } catch (e) { /* 刷失败等轮询兜底 */ }
|
|
1828
1890
|
jumpToRun(r.run_id);
|
|
1829
1891
|
$("f-goal").value = "";
|
|
1892
|
+
if ($("f-bible")) $("f-bible").value = "";
|
|
1830
1893
|
S.atts = []; renderAttachChips(); // 附件已移交任务待提交区,清空本地列表
|
|
1831
|
-
} catch (e) {
|
|
1832
|
-
msg.className = "msg err"; msg.textContent = e.message;
|
|
1833
|
-
}
|
|
1834
|
-
|
|
1894
|
+
} catch (e) {
|
|
1895
|
+
msg.className = "msg err"; msg.textContent = e.message;
|
|
1896
|
+
} finally {
|
|
1897
|
+
resetSubmit();
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1835
1900
|
|
|
1836
1901
|
/* ---------------------------------------------------------- 附件(截图/文件) */
|
|
1837
1902
|
/* 待提交附件:选中/粘贴即上传到服务端 pending 区,创建任务时随 payload 落盘
|
|
@@ -2101,44 +2166,59 @@ function openCtxMenu(x, y, items) {
|
|
|
2101
2166
|
};
|
|
2102
2167
|
}
|
|
2103
2168
|
|
|
2104
|
-
function closeCtxMenu() {
|
|
2105
|
-
const menu = $("ctx-menu");
|
|
2106
|
-
if (menu && !menu.classList.contains("hidden")) menu.classList.add("hidden");
|
|
2107
|
-
}
|
|
2108
|
-
|
|
2109
|
-
|
|
2169
|
+
function closeCtxMenu() {
|
|
2170
|
+
const menu = $("ctx-menu");
|
|
2171
|
+
if (menu && !menu.classList.contains("hidden")) menu.classList.add("hidden");
|
|
2172
|
+
}
|
|
2173
|
+
|
|
2174
|
+
/* 任务行的「⋯」与右键共用同一组动作,避免两个入口越改越不一样。
|
|
2175
|
+
* det 是 .stask 元素;菜单位置由调用方提供,缺省时贴在行右下角。 */
|
|
2176
|
+
function taskContextItems(det) {
|
|
2177
|
+
const taskId = det && det.dataset ? (det.dataset.task || "") : "";
|
|
2178
|
+
const runId = det && det.dataset ? (det.dataset.run || "") : "";
|
|
2179
|
+
const items = [];
|
|
2180
|
+
if (runId) items.push({ label: t("打开详情"), fn: () => sideOpenRun(runId) });
|
|
2181
|
+
if (taskId) {
|
|
2182
|
+
items.push("-");
|
|
2183
|
+
items.push({ label: t("打开工作目录"), fn: () => revealPath("tasks", taskId, true) });
|
|
2184
|
+
items.push({ label: t("复制工作目录路径"), fn: () => revealPath("tasks", taskId, false) });
|
|
2185
|
+
if (runId) items.push({ label: t("复制日志目录路径"), fn: () => revealPath("runs", runId, false) });
|
|
2186
|
+
const st = det.dataset.status || "";
|
|
2187
|
+
if (st === "failed" || st === "cancelled") items.push({ label: t("↻ 继续任务"), fn: () => retryTask(taskId) });
|
|
2188
|
+
items.push({ label: (st === "failed" || st === "cancelled") ? t("✎ 编辑重试") : t("基于此任务新建"),
|
|
2189
|
+
fn: () => newFromTask(taskId) });
|
|
2190
|
+
const sTask = ((S.state || {}).tasks || []).find((x) => x.id === taskId);
|
|
2191
|
+
if (sTask && sTask.serial && st !== "running" && st !== "queued")
|
|
2192
|
+
items.push({ label: t("继续连载(新任务)"), fn: () => continueSerial(taskId) });
|
|
2193
|
+
items.push({ label: t("重命名任务"), fn: () => renameTask(taskId) });
|
|
2194
|
+
const archived = archivedTaskIds().has(taskId);
|
|
2195
|
+
items.push({ label: archived ? t("取消归档") : t("归档"), fn: () => archiveTask(taskId, !archived) });
|
|
2196
|
+
items.push({ label: t("删除任务"), danger: true, fn: () => deleteTask(taskId) });
|
|
2197
|
+
} else if (runId) {
|
|
2198
|
+
items.push("-");
|
|
2199
|
+
items.push({ label: t("复制日志目录路径"), fn: () => revealPath("runs", runId, false) });
|
|
2200
|
+
items.push({ label: t("删除记录"), danger: true, fn: () => deleteRun(runId) });
|
|
2201
|
+
}
|
|
2202
|
+
return items;
|
|
2203
|
+
}
|
|
2204
|
+
|
|
2205
|
+
window.openTaskActions = function (det, ev) {
|
|
2206
|
+
if (!det) return;
|
|
2207
|
+
const r = det.getBoundingClientRect();
|
|
2208
|
+
const x = ev && Number.isFinite(ev.clientX) ? ev.clientX : r.right - 8;
|
|
2209
|
+
const y = ev && Number.isFinite(ev.clientY) ? ev.clientY : r.bottom;
|
|
2210
|
+
openCtxMenu(x, y, taskContextItems(det));
|
|
2211
|
+
};
|
|
2212
|
+
|
|
2213
|
+
function bindCtxMenus() {
|
|
2110
2214
|
// 侧栏任务树:右键任务 → 详情/目录/重试/重命名/归档/删除;管理类运行 → 详情/目录/删除记录
|
|
2111
2215
|
$("side-tasks").addEventListener("contextmenu", (e) => {
|
|
2112
2216
|
// .stask 在 .sdir 内部:先判任务行,点到才算文件夹自己的菜单
|
|
2113
|
-
const det = e.target.closest(".stask");
|
|
2114
|
-
if (det) {
|
|
2115
|
-
e.preventDefault();
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
if (runId) items.push({ label: t("打开详情"), fn: () => sideOpenRun(runId) });
|
|
2119
|
-
if (taskId) {
|
|
2120
|
-
items.push("-");
|
|
2121
|
-
items.push({ label: t("打开工作目录"), fn: () => revealPath("tasks", taskId, true) });
|
|
2122
|
-
items.push({ label: t("复制工作目录路径"), fn: () => revealPath("tasks", taskId, false) });
|
|
2123
|
-
if (runId) items.push({ label: t("复制日志目录路径"), fn: () => revealPath("runs", runId, false) });
|
|
2124
|
-
const st = det.dataset.status || "";
|
|
2125
|
-
if (st === "failed" || st === "cancelled") items.push({ label: t("↻ 继续任务"), fn: () => retryTask(taskId) });
|
|
2126
|
-
// 与详情页同一套说法:失败/取消叫「编辑重试」,其余叫「基于此任务新建」
|
|
2127
|
-
items.push({ label: (st === "failed" || st === "cancelled") ? t("✎ 编辑重试") : t("基于此任务新建"),
|
|
2128
|
-
fn: () => newFromTask(taskId) });
|
|
2129
|
-
const sTask = ((S.state || {}).tasks || []).find((x) => x.id === taskId);
|
|
2130
|
-
if (sTask && sTask.serial && st !== "running" && st !== "queued")
|
|
2131
|
-
items.push({ label: t("继续连载(新任务)"), fn: () => continueSerial(taskId) });
|
|
2132
|
-
items.push({ label: t("重命名任务"), fn: () => renameTask(taskId) });
|
|
2133
|
-
items.push({ label: archivedTaskIds().has(taskId) ? t("取消归档") : t("归档"), fn: () => archiveTask(taskId, !archivedTaskIds().has(taskId)) });
|
|
2134
|
-
items.push({ label: t("删除任务"), danger: true, fn: () => deleteTask(taskId) });
|
|
2135
|
-
} else if (runId) {
|
|
2136
|
-
items.push("-");
|
|
2137
|
-
items.push({ label: t("复制日志目录路径"), fn: () => revealPath("runs", runId, false) });
|
|
2138
|
-
items.push({ label: t("删除记录"), danger: true, fn: () => deleteRun(runId) });
|
|
2139
|
-
}
|
|
2140
|
-
openCtxMenu(e.clientX, e.clientY, items);
|
|
2141
|
-
return;
|
|
2217
|
+
const det = e.target.closest(".stask");
|
|
2218
|
+
if (det) {
|
|
2219
|
+
e.preventDefault();
|
|
2220
|
+
openCtxMenu(e.clientX, e.clientY, taskContextItems(det));
|
|
2221
|
+
return;
|
|
2142
2222
|
}
|
|
2143
2223
|
// 文件夹行(ZCode 桌面端式样):新建任务到该目录 / 打开工作目录 / 复制路径 / 移除。
|
|
2144
2224
|
// 「其他」是兜底展示(无主运行的聚合),不是真实目录,不弹菜单。
|
|
@@ -2803,17 +2883,19 @@ function renderSideTasks() {
|
|
|
2803
2883
|
// 二级任务行:单行即全部——点击主栏直开任务详情。
|
|
2804
2884
|
// 内嵌步骤层已砍:步骤/运行明细在任务详情里更全,侧栏只留状态字形 + 徽章 + 相对时间。
|
|
2805
2885
|
// 右键菜单仍吃 data-task / data-run;主栏开着某任务详情时行保持高亮。
|
|
2806
|
-
const row = (g) => {
|
|
2807
|
-
if (g.active) g.status = "running";
|
|
2808
|
-
const sel = (g.key === S.detailTaskKey || g.runIds.indexOf(S.detailRunId) >= 0) ? " active" : "";
|
|
2809
|
-
return '<div class="stask' + sel + (g.archived ? " archived" : "") + '" tabindex="0" role="button" data-key="' + esc(g.key) +
|
|
2810
|
-
'" data-task="' + esc(g.taskId || "") + '" data-run="' + esc(g.runIds[0] || "") +
|
|
2811
|
-
'" data-status="' + esc(g.status || "") + '">' + staskGlyph(g.status) +
|
|
2812
|
-
'<span class="t">' + esc(g.title) + "</span>" +
|
|
2813
|
-
(g.archived ? '<span class="sbadge archb">' + t("已归档") + "</span>" : "") +
|
|
2814
|
-
(g.verdict ? '<span class="sbadge">' + t("待裁决") + "</span>" : "") +
|
|
2815
|
-
'<span class="tm">' + esc(relTime(g.time)) + "</span
|
|
2816
|
-
|
|
2886
|
+
const row = (g) => {
|
|
2887
|
+
if (g.active) g.status = "running";
|
|
2888
|
+
const sel = (g.key === S.detailTaskKey || g.runIds.indexOf(S.detailRunId) >= 0) ? " active" : "";
|
|
2889
|
+
return '<div class="stask' + sel + (g.archived ? " archived" : "") + '" tabindex="0" role="button" data-key="' + esc(g.key) +
|
|
2890
|
+
'" data-task="' + esc(g.taskId || "") + '" data-run="' + esc(g.runIds[0] || "") +
|
|
2891
|
+
'" data-status="' + esc(g.status || "") + '">' + staskGlyph(g.status) +
|
|
2892
|
+
'<span class="t">' + esc(g.title) + "</span>" +
|
|
2893
|
+
(g.archived ? '<span class="sbadge archb">' + t("已归档") + "</span>" : "") +
|
|
2894
|
+
(g.verdict ? '<span class="sbadge">' + t("待裁决") + "</span>" : "") +
|
|
2895
|
+
'<span class="tm">' + esc(relTime(g.time)) + "</span>" +
|
|
2896
|
+
'<button class="stask-more" type="button" aria-haspopup="menu" aria-label="' + esc(t("任务操作")) + '" title="' + esc(t("任务操作")) +
|
|
2897
|
+
'" onclick="event.stopPropagation();openTaskActions(this.closest(\'.stask\'), event)">⋯</button></div>';
|
|
2898
|
+
};
|
|
2817
2899
|
// 一级文件夹行:折叠箭头 + 目录图标 + 末段名(hover 显全路径)+ 任务计数徽标
|
|
2818
2900
|
box.innerHTML = dirs.map((d) => {
|
|
2819
2901
|
const isOrphan = d.dir === ORPHAN;
|
|
@@ -2963,7 +3045,7 @@ function drawTaskDetail(key, runs) {
|
|
|
2963
3045
|
if (sig === S.taskSig) return;
|
|
2964
3046
|
S.taskSig = sig;
|
|
2965
3047
|
const latest = runs[0]; // runs 新→旧
|
|
2966
|
-
const
|
|
3048
|
+
const ordered = runs.slice(); // 详情按最近运行优先,方便排查
|
|
2967
3049
|
const totalSteps = runs.reduce((a, r) => a + (r.steps || []).length, 0);
|
|
2968
3050
|
const active = runs.some((r) => r.status === "running" || r.status === "queued");
|
|
2969
3051
|
const st = active ? "running" : latest.status;
|
|
@@ -2977,7 +3059,9 @@ function drawTaskDetail(key, runs) {
|
|
|
2977
3059
|
$("btn-retry").classList.toggle("hidden", !(latest.task_id && (latest.status === "failed" || latest.status === "cancelled")));
|
|
2978
3060
|
setEditRetry(latest.task_id, latest.status,
|
|
2979
3061
|
!!((S.state || {}).tasks || []).some((x) => x.id === latest.task_id));
|
|
2980
|
-
S.lastRun = latest;
|
|
3062
|
+
S.lastRun = latest;
|
|
3063
|
+
renderRunOutcome(latest, runs);
|
|
3064
|
+
renderDetailOverview(latest, runs, bmTask);
|
|
2981
3065
|
// 指挥区指向最新的活跃运行(无则隐藏整个指挥区,避免终态任务误导用户以为指令还能生效)
|
|
2982
3066
|
const activeRun = runs.find((r) => r.status === "queued" || r.status === "running");
|
|
2983
3067
|
// 活跃运行(含排队中)显示取消按钮;取消目标钉在活跃 run——任务级详情没有
|
|
@@ -3002,20 +3086,23 @@ function drawTaskDetail(key, runs) {
|
|
|
3002
3086
|
$("rd-plan").classList.add("hidden");
|
|
3003
3087
|
const statusTxt = { queued: t("排队中"), running: t("运行中"), done: t("完成"), failed: t("失败"), cancelled: t("已取消") };
|
|
3004
3088
|
let html = "";
|
|
3005
|
-
|
|
3006
|
-
|
|
3089
|
+
ordered.forEach((r, i) => {
|
|
3090
|
+
const runNo = runs.length - i;
|
|
3091
|
+
html += '<div class="run-sep"><span class="rs-i">' + t("第 ") + runNo + "/" + runs.length + t(" 次运行") + '</span>' +
|
|
3007
3092
|
'<span class="rs-t">' + esc(String(r.created_at || "").slice(5, 16)) + "</span>" +
|
|
3008
3093
|
'<span class="chip ' + esc(r.status || "") + '">' + (statusTxt[r.status] || r.status || "") + "</span>" +
|
|
3009
3094
|
(r.error ? '<span class="rs-err" title="' + esc(r.error.slice(0, 200)) + '">' + esc(r.error.slice(0, 60)) + "</span>" : "") +
|
|
3010
3095
|
"</div>";
|
|
3011
|
-
html += (r.steps || []).map((s) =>
|
|
3012
|
-
'<div class="step"
|
|
3096
|
+
html += (r.steps || []).slice().reverse().map((s) =>
|
|
3097
|
+
'<div class="step" data-n="' + Number(s.n) + '" data-run-id="' + esc(r.id) + '" data-log="' + esc(s.log || "") + '" title="' + esc(s.note || "") + '">' +
|
|
3013
3098
|
'<span class="n">' + String(s.n).padStart(2, "0") + "</span>" +
|
|
3014
3099
|
'<span class="role">' + esc(s.role) + "</span>" +
|
|
3015
3100
|
'<span class="who">' + esc(t(s.agent_label || s.agent)) + "</span>" +
|
|
3016
3101
|
'<span class="sum">' + esc((s.note ? "◆ " + s.note + " — " : "") + (s.summary || "")) + "</span>" +
|
|
3017
3102
|
'<span class="dur">' + (s.duration_s != null ? s.duration_s + "s" : "") + "</span>" +
|
|
3018
|
-
statusChip(s.status) +
|
|
3103
|
+
statusChip(s.status) +
|
|
3104
|
+
(s.log ? '<button class="step-log-btn" type="button" data-log-run="' + esc(r.id) + '" data-log-rel="' + esc(s.log) + '" title="' + esc(t("查看日志")) + '">' + esc(t("CLI 日志")) + "</button>" : "") +
|
|
3105
|
+
"</div>"
|
|
3019
3106
|
).join("");
|
|
3020
3107
|
});
|
|
3021
3108
|
$("rd-steps").innerHTML = html || '<div class="empty">' + t("尚无步骤") + '</div>';
|
|
@@ -3158,7 +3245,7 @@ function applyStepFocus() {
|
|
|
3158
3245
|
el.classList.add("flash");
|
|
3159
3246
|
try { el.scrollIntoView({ block: "center", behavior: "smooth" }); }
|
|
3160
3247
|
catch (e) { el.scrollIntoView(); }
|
|
3161
|
-
if (el.dataset.log) toggleLog(S.detailRunId, el.dataset.log);
|
|
3248
|
+
if (el.dataset.log) toggleLog(el.dataset.runId || S.detailRunId, el.dataset.log);
|
|
3162
3249
|
setTimeout(() => el.classList.remove("flash"), 1800);
|
|
3163
3250
|
}
|
|
3164
3251
|
|
|
@@ -3192,6 +3279,18 @@ function applyRdTabs() {
|
|
|
3192
3279
|
});
|
|
3193
3280
|
document.querySelectorAll("#run-detail .rd-pane").forEach((p) =>
|
|
3194
3281
|
p.classList.toggle("hidden", p.dataset.pane !== S.rdTab));
|
|
3282
|
+
document.querySelectorAll("#rd-tabs .rd-tab").forEach((b) => {
|
|
3283
|
+
const active = b.dataset.tab === S.rdTab && !b.classList.contains("hidden");
|
|
3284
|
+
b.setAttribute("aria-selected", active ? "true" : "false");
|
|
3285
|
+
b.tabIndex = active ? 0 : -1;
|
|
3286
|
+
});
|
|
3287
|
+
document.querySelectorAll("#run-detail .rd-pane").forEach((p) =>
|
|
3288
|
+
p.setAttribute("aria-hidden", p.dataset.pane === S.rdTab ? "false" : "true"));
|
|
3289
|
+
const focusedTab = document.activeElement;
|
|
3290
|
+
if (focusedTab && focusedTab.matches && focusedTab.matches("#rd-tabs .rd-tab.hidden")) {
|
|
3291
|
+
const nextTab = document.querySelector('#rd-tabs .rd-tab[data-tab="' + S.rdTab + '"]:not(.hidden)');
|
|
3292
|
+
if (nextTab) nextTab.focus({ preventScroll: true });
|
|
3293
|
+
}
|
|
3195
3294
|
// 对话页签不放日志抽屉:会盖住贴底输入条(手动切来/自动选卡都覆盖)
|
|
3196
3295
|
if (S.rdTab === "chat" && !$("rd-log").classList.contains("hidden")) window.rdLogClose();
|
|
3197
3296
|
renderChatNav();
|
|
@@ -3267,13 +3366,16 @@ function rdTabsSync(ctx) {
|
|
|
3267
3366
|
const sig = (S.detailTaskKey || S.detailRunId || "") + "|" + (ctx.status || "") +
|
|
3268
3367
|
"|" + (ctx.gitState || "") + "|" + (ctx.running ? 1 : 0) + "|" + (chatAvail ? 1 : 0);
|
|
3269
3368
|
if (S.rdTabSig !== sig) {
|
|
3270
|
-
S.rdTabSig = sig;
|
|
3271
|
-
const avail = rdTabAvail();
|
|
3272
|
-
|
|
3273
|
-
//
|
|
3274
|
-
//
|
|
3275
|
-
|
|
3276
|
-
|
|
3369
|
+
S.rdTabSig = sig;
|
|
3370
|
+
const avail = rdTabAvail();
|
|
3371
|
+
const direct = chatAvail && chatEngineIsDirect(S.lastRun);
|
|
3372
|
+
// 直连任务的主问题是「继续聊什么、上一轮回答是什么」——默认把对话
|
|
3373
|
+
// 放在第一视线;编排/代码任务才默认落蜂巢,先看阶段与在岗步骤。
|
|
3374
|
+
// 没跑到终态时成果分区是空的,避免打开详情先看到白板。
|
|
3375
|
+
const finishing = !ctx.running && ["done", "failed", "cancelled", "timeout"].includes(ctx.status);
|
|
3376
|
+
S.rdTab = (direct && avail.chat ? "chat" : null)
|
|
3377
|
+
|| (avail.hive ? "hive" : null)
|
|
3378
|
+
|| (avail.chat ? "chat" : null)
|
|
3277
3379
|
|| (ctx.running ? (avail.hive ? "hive" : "steps")
|
|
3278
3380
|
: (ctx.gitState === "isolated" && avail.git ? "git"
|
|
3279
3381
|
: (ctx.hasResult && finishing ? "result" : "steps")));
|
|
@@ -3313,7 +3415,9 @@ async function renderRunDetail() {
|
|
|
3313
3415
|
$("btn-continue").classList.toggle("hidden",
|
|
3314
3416
|
!(rcTask && rcTask.serial && run.status !== "running" && run.status !== "queued"));
|
|
3315
3417
|
setEditRetry(run.task_id, run.status, !!rcTask);
|
|
3316
|
-
S.lastRun = run;
|
|
3418
|
+
S.lastRun = run;
|
|
3419
|
+
renderRunOutcome(run);
|
|
3420
|
+
renderDetailOverview(run, [run], rcTask);
|
|
3317
3421
|
const bp = $("btn-pause");
|
|
3318
3422
|
if (bp) { bp.classList.toggle("hidden", !active);
|
|
3319
3423
|
bp.textContent = run.paused ? t("继续执行") : t("暂停"); }
|
|
@@ -3329,16 +3433,20 @@ async function renderRunDetail() {
|
|
|
3329
3433
|
'<span class="stat tasksum hidden" id="rd-meta-task"></span>';
|
|
3330
3434
|
if (S.detailSide) fillMetaTask(S.detailSide.stats || {}); // 缓存命中:轮询重画不闪丢累计组
|
|
3331
3435
|
renderPlan(run);
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3436
|
+
// 详情页优先展示最近一步,排查运行中的任务时无需滚到底部;
|
|
3437
|
+
// 蜂巢泳道仍按原始流程顺序呈现,避免破坏阶段语义。
|
|
3438
|
+
$("rd-steps").innerHTML = (run.steps || []).slice().reverse().map((s) =>
|
|
3439
|
+
'<div class="step" data-n="' + Number(s.n) + '" data-run-id="' + esc(run.id) + '" data-log="' + esc(s.log || "") +
|
|
3440
|
+
'" title="' + esc(s.note || "") + '">' +
|
|
3335
3441
|
'<span class="n">' + String(s.n).padStart(2, "0") + "</span>" +
|
|
3336
3442
|
'<span class="role">' + esc(s.role) + "</span>" +
|
|
3337
|
-
'<span class="who">' + esc(t(s.agent_label || s.agent)) + "</span>" +
|
|
3338
|
-
'<span class="sum">' + esc((s.note ? "◆ " + s.note + " — " : "") + (s.summary || "")) + "</span>" +
|
|
3339
|
-
'<span class="dur">' + (s.duration_s != null ? s.duration_s + "s" : "") + "</span>" +
|
|
3340
|
-
statusChip(s.status) +
|
|
3341
|
-
|
|
3443
|
+
'<span class="who">' + esc(t(s.agent_label || s.agent)) + "</span>" +
|
|
3444
|
+
'<span class="sum">' + esc((s.note ? "◆ " + s.note + " — " : "") + (s.summary || "")) + "</span>" +
|
|
3445
|
+
'<span class="dur">' + (s.duration_s != null ? s.duration_s + "s" : "") + "</span>" +
|
|
3446
|
+
statusChip(s.status) +
|
|
3447
|
+
(s.log ? '<button class="step-log-btn" type="button" data-log-run="' + esc(run.id) + '" data-log-rel="' + esc(s.log) + '" title="' + esc(t("查看日志")) + '">' + esc(t("CLI 日志")) + "</button>" : "") +
|
|
3448
|
+
"</div>"
|
|
3449
|
+
).join("") || '<div class="empty">' + t("尚无步骤") + '</div>';
|
|
3342
3450
|
applyStepFocus();
|
|
3343
3451
|
// 报告
|
|
3344
3452
|
if (run.status === "done" || run.report) {
|
|
@@ -4545,9 +4653,10 @@ function bindInspector() {
|
|
|
4545
4653
|
const det = e.target.closest(".stask");
|
|
4546
4654
|
if (det) sideRowActivate(det);
|
|
4547
4655
|
});
|
|
4548
|
-
$("side-tasks").addEventListener("keydown", (e) => {
|
|
4549
|
-
if (e.key !== "Enter" && e.key !== " ") return;
|
|
4550
|
-
|
|
4656
|
+
$("side-tasks").addEventListener("keydown", (e) => {
|
|
4657
|
+
if (e.key !== "Enter" && e.key !== " ") return;
|
|
4658
|
+
if (e.target.closest("button, a, input, select, textarea, summary")) return;
|
|
4659
|
+
const det = e.target.closest(".stask");
|
|
4551
4660
|
if (det) { e.preventDefault(); sideRowActivate(det); }
|
|
4552
4661
|
});
|
|
4553
4662
|
}
|
|
@@ -4567,14 +4676,13 @@ function artifactsChips(runId, files) {
|
|
|
4567
4676
|
return files.map((f) => {
|
|
4568
4677
|
const isTxt = FP_TXT.has(_fpExt(f.name)); // 文本/代码都有弹窗预览(按代码格式展示)
|
|
4569
4678
|
return '<span class="fc-row">' +
|
|
4570
|
-
'<a class="file-chip" href="/api/runs/' + encodeURIComponent(runId) + "/file?name=" +
|
|
4571
|
-
encodeURIComponent(f.name) + '" target="_blank" rel="noopener" ' +
|
|
4572
|
-
'title="' + esc(f.name + " · " + fmtSize(f.size)) + '"
|
|
4573
|
-
esc(f.name) + "', " + (Number(f.size) || 0) + ');return false">' +
|
|
4679
|
+
'<a class="file-chip artifact-file-open" href="/api/runs/' + encodeURIComponent(runId) + "/file?name=" +
|
|
4680
|
+
encodeURIComponent(f.name) + '" target="_blank" rel="noopener" ' +
|
|
4681
|
+
'title="' + esc(f.name + " · " + fmtSize(f.size)) + '" data-file-run="' + esc(runId) + '" data-file-name="' + esc(f.name) + '" data-file-size="' + (Number(f.size) || 0) + '">' +
|
|
4574
4682
|
'<i class="fx">' + esc(_fpExt(f.name).slice(0, 4) || "file") + "</i>" +
|
|
4575
4683
|
'<span class="p">' + esc(f.name) + "</span><i>" + fmtSize(f.size) + "</i></a>" +
|
|
4576
|
-
(isTxt ? '<a class="file-chip prev" title="' + esc(t("查看内容")) +
|
|
4577
|
-
'"
|
|
4684
|
+
(isTxt ? '<a class="file-chip prev artifact-file-preview" title="' + esc(t("查看内容")) +
|
|
4685
|
+
'" data-file-run="' + esc(runId) + '" data-file-name="' + esc(f.name) + '" data-file-size="' + (Number(f.size) || 0) + '">' +
|
|
4578
4686
|
'<svg class="ico" aria-hidden="true"><use href="#i-book"/></svg>' + t("预览") + "</a>" : "") +
|
|
4579
4687
|
"</span>";
|
|
4580
4688
|
}).join("");
|
|
@@ -4625,12 +4733,14 @@ window.copyText = function (t) {
|
|
|
4625
4733
|
} catch (e) { /* 剪贴板不可用则忽略 */ }
|
|
4626
4734
|
};
|
|
4627
4735
|
|
|
4628
|
-
let currentLog = null;
|
|
4629
|
-
|
|
4630
|
-
/* 当前打开的日志是否仍属于这批步骤(重画后日志路径消失说明步骤被重建,才收起) */
|
|
4631
|
-
function stepsMatch(runs,
|
|
4632
|
-
|
|
4633
|
-
|
|
4736
|
+
let currentLog = null; // { runId, rel }:同一路径在不同轮次也必须能切换
|
|
4737
|
+
|
|
4738
|
+
/* 当前打开的日志是否仍属于这批步骤(重画后日志路径消失说明步骤被重建,才收起) */
|
|
4739
|
+
function stepsMatch(runs, current) {
|
|
4740
|
+
if (!current) return false;
|
|
4741
|
+
return (runs || []).some((r) => String(r.id || "") === String(current.runId || "") &&
|
|
4742
|
+
(r.steps || []).some((s) => s.log === current.rel));
|
|
4743
|
+
}
|
|
4634
4744
|
|
|
4635
4745
|
function renderPlan(run) {
|
|
4636
4746
|
const box = $("rd-plan");
|
|
@@ -4691,22 +4801,24 @@ window.rdLogClose = function () {
|
|
|
4691
4801
|
if (st) st.textContent = "";
|
|
4692
4802
|
};
|
|
4693
4803
|
|
|
4694
|
-
async function toggleLog(runId, rel) {
|
|
4695
|
-
const box = $("rd-log"), pre = $("rd-log-text");
|
|
4696
|
-
if (currentLog === rel && !box.classList.contains("hidden")) {
|
|
4804
|
+
async function toggleLog(runId, rel) {
|
|
4805
|
+
const box = $("rd-log"), pre = $("rd-log-text");
|
|
4806
|
+
if (currentLog && currentLog.runId === runId && currentLog.rel === rel && !box.classList.contains("hidden")) {
|
|
4697
4807
|
window.rdLogClose(); return;
|
|
4698
4808
|
}
|
|
4699
4809
|
try {
|
|
4700
4810
|
const r = await api("/api/runs/" + encodeURIComponent(runId) + "/log?step=" + encodeURIComponent(rel) + "&pretty=1");
|
|
4701
4811
|
// 拉取期间可能切详情:过期日志不落盘(A 的步骤输出写进 B 详情的抽屉)。
|
|
4702
4812
|
// 检查器蜂巢/步骤点开日志同款:run 属检查器当前 run 也算在场
|
|
4703
|
-
const owned = S.detailRunId === runId ||
|
|
4704
|
-
|
|
4705
|
-
|
|
4813
|
+
const owned = S.detailRunId === runId ||
|
|
4814
|
+
// 任务级直连时间线会跨多轮 run 回放;按钮上的 run 可能不是最新一轮,
|
|
4815
|
+
// 只按 latest.id 判断会让旧轮次的「CLI 日志」点击后悄悄失效。
|
|
4816
|
+
(S.detailTaskKey && S.lastRun && S.lastRun.task_id === S.detailTaskKey) ||
|
|
4817
|
+
((S.inspData || {}).run || {}).id === runId;
|
|
4706
4818
|
if (!owned) return;
|
|
4707
4819
|
pre.textContent = r.log || t("(等待输出…)");
|
|
4708
4820
|
box.classList.remove("hidden");
|
|
4709
|
-
currentLog = rel;
|
|
4821
|
+
currentLog = { runId, rel };
|
|
4710
4822
|
const st = $("rd-log-step");
|
|
4711
4823
|
if (st) st.textContent = rdLogStepLabel(runId, rel);
|
|
4712
4824
|
pre.scrollTop = pre.scrollHeight; // 打开即看最新输出
|
|
@@ -4716,10 +4828,10 @@ async function toggleLog(runId, rel) {
|
|
|
4716
4828
|
stopLogLive();
|
|
4717
4829
|
if (!live) return;
|
|
4718
4830
|
S.logLive = setInterval(async () => {
|
|
4719
|
-
if (!currentLog || currentLog !== rel) return stopLogLive();
|
|
4831
|
+
if (!currentLog || currentLog.runId !== runId || currentLog.rel !== rel) return stopLogLive();
|
|
4720
4832
|
try {
|
|
4721
4833
|
const rr = await api("/api/runs/" + encodeURIComponent(runId) + "/log?step=" + encodeURIComponent(rel) + "&pretty=1");
|
|
4722
|
-
if (currentLog === rel) {
|
|
4834
|
+
if (currentLog && currentLog.runId === runId && currentLog.rel === rel) {
|
|
4723
4835
|
// 贴底跟随:用户滚到底部附近才自动滚到最新输出,回看历史不打扰
|
|
4724
4836
|
const stick = pre.scrollHeight - pre.scrollTop - pre.clientHeight < 48;
|
|
4725
4837
|
pre.textContent = rr.log || t("(等待输出…)");
|
|
@@ -4803,8 +4915,16 @@ window.renderHive = function (run) {
|
|
|
4803
4915
|
});
|
|
4804
4916
|
const running = steps.filter((s) => s.status === "running");
|
|
4805
4917
|
const sub = $("rd-hive-sub");
|
|
4806
|
-
if (sub)
|
|
4807
|
-
|
|
4918
|
+
if (sub) {
|
|
4919
|
+
const done = steps.filter((s) => ["done", "completed", "success"].includes(String(s.status || "").toLowerCase())).length;
|
|
4920
|
+
const issues = steps.filter((s) => ["failed", "timeout", "cancelled"].includes(String(s.status || "").toLowerCase())).length;
|
|
4921
|
+
sub.innerHTML = '<span class="hive-count hive-count-live"><i class="live-dot"></i>' +
|
|
4922
|
+
esc(t("在岗")) + " " + running.length + '</span>' +
|
|
4923
|
+
'<span class="hive-count hive-count-done">✓ ' + done + '</span>' +
|
|
4924
|
+
(issues ? '<span class="hive-count hive-count-issue">! ' + issues + '</span>' : "") +
|
|
4925
|
+
'<span class="hive-count hive-count-total">' + steps.length + ' ' + esc(t("步骤")) + '</span>' +
|
|
4926
|
+
'<span class="hive-hint">' + esc(t("点击步骤卡片查看 CLI 日志")) + '</span>';
|
|
4927
|
+
}
|
|
4808
4928
|
const activeIdx = lanes.map((l) => byStage[l].some((s) => s.status === "running")).lastIndexOf(true);
|
|
4809
4929
|
const cell = (s, laneIdx, cellIdx) => {
|
|
4810
4930
|
// 五归一:取消/超时的格子不再冒充「完成」——与步骤芯片同三色体系
|
|
@@ -4825,10 +4945,11 @@ window.renderHive = function (run) {
|
|
|
4825
4945
|
(s.note ? "◆ " + s.note : "")].filter(Boolean).join(" · ")
|
|
4826
4946
|
+ (concl && st !== "running" ? "\n" + t("结论:") + concl : "");
|
|
4827
4947
|
// --d:入场瀑布逐格延迟(泳道间 110ms + 同泳道逐格 45ms),样式端消费
|
|
4828
|
-
return '<div class="hive-cell st-' + st + (st === "running" ? " hc-breathe" : "") + '"' +
|
|
4829
|
-
'
|
|
4830
|
-
'
|
|
4831
|
-
'
|
|
4948
|
+
return '<div class="hive-cell st-' + st + (st === "running" ? " hc-breathe" : "") + '" role="button" tabindex="0"' +
|
|
4949
|
+
' data-hive-run="' + esc(run.id) + '" data-hive-log="' + esc(s.log || "") + '"' +
|
|
4950
|
+
' style="--d:' + (laneIdx * 110 + cellIdx * 45) + 'ms"' +
|
|
4951
|
+
' title="' + esc(title) + '" ' +
|
|
4952
|
+
'>' +
|
|
4832
4953
|
'<div class="hc-head">' +
|
|
4833
4954
|
'<img class="hc-bee" src="icons/bee.svg" alt="" aria-hidden="true">' +
|
|
4834
4955
|
'<span class="hc-role">' + esc(s.role || "") + "</span>" +
|
|
@@ -4838,9 +4959,10 @@ window.renderHive = function (run) {
|
|
|
4838
4959
|
'<div class="hc-meta"><span class="hc-elapsed" data-started="' + esc(s.started_at || "") + '">' +
|
|
4839
4960
|
(s.duration_s != null ? s.duration_s + "s" : hiveElapsed(s.started_at)) + "</span>" +
|
|
4840
4961
|
(st === "running" ? '<span class="hc-live"><i class="live-dot"></i>' + t("工作中") + "</span>" : "") +
|
|
4841
|
-
(st === "timeout" ? '<span class="hc-dead">⏱ ' + t("超时") + "</span>" : "") +
|
|
4842
|
-
(st === "cancelled" ? '<span class="hc-dead">' + t("已取消") + "</span>" : "") +
|
|
4843
|
-
"</
|
|
4962
|
+
(st === "timeout" ? '<span class="hc-dead">⏱ ' + t("超时") + "</span>" : "") +
|
|
4963
|
+
(st === "cancelled" ? '<span class="hc-dead">' + t("已取消") + "</span>" : "") +
|
|
4964
|
+
(s.log ? '<span class="hc-log-link">' + esc(t("CLI 日志")) + "</span>" : "") +
|
|
4965
|
+
"</div></div>";
|
|
4844
4966
|
};
|
|
4845
4967
|
const dots = (list, laneIdx) => list.slice(-12).map((s, di) => {
|
|
4846
4968
|
const dc = s.status === "running" ? "run" : s.status === "failed" ? "fail"
|
|
@@ -4859,12 +4981,14 @@ window.renderHive = function (run) {
|
|
|
4859
4981
|
: (activeIdx === -1 || i < activeIdx ? "lane-done" : "lane-idle");
|
|
4860
4982
|
// 泳道头视觉 v2:阶段序号徽章(01/02…流水线站序)+ 已完成进度(settled/total)
|
|
4861
4983
|
const settled = list.filter((s) => !["running", "queued"].includes(s.status)).length;
|
|
4984
|
+
const meter = list.length ? Math.round((settled / list.length) * 100) : 0;
|
|
4862
4985
|
return '<div class="hive-lane ' + cls + '">' +
|
|
4863
4986
|
'<div class="lane-head">' +
|
|
4864
4987
|
'<span class="lane-idx">' + String(i + 1).padStart(2, "0") + "</span>" +
|
|
4865
4988
|
'<span class="lane-name">' + esc(stage) + '</span>' +
|
|
4866
4989
|
'<span class="lane-n">×' + list.length + "</span>" +
|
|
4867
4990
|
(settled ? '<span class="lane-prog">' + settled + "/" + list.length + "</span>" : "") +
|
|
4991
|
+
'<span class="lane-meter" aria-label="' + meter + '%"><i style="width:' + meter + '%"></i></span>' +
|
|
4868
4992
|
(hasRun ? '<span class="lane-live"><i class="live-dot"></i>' + t("进行中") + "</span>" : "") + "</div>" +
|
|
4869
4993
|
'<div class="lane-track">' + dots(list, i) + "</div>" +
|
|
4870
4994
|
'<div class="lane-cells">' + list.slice(-8).map((s, ci) => cell(s, i, ci)).join("") + "</div></div>";
|
|
@@ -5166,9 +5290,10 @@ async function renderChat(run, active) {
|
|
|
5166
5290
|
if (sig === chatSig) return; // 轮询重画去抖:内容没变不重建 DOM(保住输入焦点)
|
|
5167
5291
|
const runForFetch = run.id; // 拉取期间可能切详情:过期响应不落盘(同 renderRunDetail 闸门)
|
|
5168
5292
|
chatSig = sig;
|
|
5169
|
-
let
|
|
5170
|
-
try {
|
|
5171
|
-
catch (e) {
|
|
5293
|
+
let data = null;
|
|
5294
|
+
try { data = await api("/api/runs/" + encodeURIComponent(run.id) + "/timeline"); }
|
|
5295
|
+
catch (e) { data = null; }
|
|
5296
|
+
const items = (data && data.items) || [];
|
|
5172
5297
|
if (!$("run-detail") || $("run-detail").classList.contains("hidden")) return;
|
|
5173
5298
|
// 过期闸门:run 级(renderRunDetail)与任务级(drawTaskDetail)都经 S.lastRun
|
|
5174
5299
|
// 指向当前详情正在展示的那条 run——拉取期间切了详情就丢弃响应
|
|
@@ -5181,7 +5306,7 @@ async function renderChat(run, active) {
|
|
|
5181
5306
|
const attName = (a) => { const p = typeof a === "string" ? a
|
|
5182
5307
|
: ((a && (a.name || a.path)) || "");
|
|
5183
5308
|
return String(p).split(/[\\/]/).pop() || ""; };
|
|
5184
|
-
|
|
5309
|
+
let html = items.map((it) => {
|
|
5185
5310
|
if (it.kind === "user") {
|
|
5186
5311
|
// 用户消息:右侧浅灰圆角块,元信息在块内顶部。
|
|
5187
5312
|
// 附件胶囊可点开预览:data-att 带 _attachments/ 相对路径(老数据裸文件名
|
|
@@ -5217,21 +5342,183 @@ async function renderChat(run, active) {
|
|
|
5217
5342
|
// 元信息只留 名字+时间(+失败标记):路由/供应商细节属于「步骤」页签,塞这里就是噪音
|
|
5218
5343
|
const metaBad = it.status === "failed" || it.status === "cancelled";
|
|
5219
5344
|
// 智能体回复:头像 + 整幅正文(不套气泡框),元信息小字落在正文下方
|
|
5220
|
-
return '<div class="chat-row">' +
|
|
5221
|
-
'<span class="chat-avatar" aria-hidden="true"><svg class="ico"><use href="#i-bee"></use></svg></span>' +
|
|
5222
|
-
'<div class="chat-bubble agent">' +
|
|
5223
|
-
'<div class="chat-body">' + bodyHtml + "</div>" +
|
|
5224
|
-
'<div class="chat-meta">' + esc(it.who || "") + " " + esc(chatTime(it.at)) +
|
|
5225
|
-
(metaBad ? " · " + t(it.status === "failed" ? "失败" : "已取消") : "") + "</div
|
|
5226
|
-
|
|
5345
|
+
return '<div class="chat-row">' +
|
|
5346
|
+
'<span class="chat-avatar" aria-hidden="true"><svg class="ico"><use href="#i-bee"></use></svg></span>' +
|
|
5347
|
+
'<div class="chat-bubble agent">' +
|
|
5348
|
+
'<div class="chat-body">' + bodyHtml + "</div>" +
|
|
5349
|
+
'<div class="chat-meta">' + esc(it.who || "") + " " + esc(chatTime(it.at)) +
|
|
5350
|
+
(metaBad ? " · " + t(it.status === "failed" ? "失败" : "已取消") : "") + "</div>" +
|
|
5351
|
+
(it.log && it.run ? '<div class="chat-actions"><button class="chat-log-btn" type="button" data-chat-log-run="' +
|
|
5352
|
+
esc(it.run) + '" data-chat-log-rel="' + esc(it.log) + '" title="' + esc(t("查看日志")) + '">' + esc(t("CLI 日志")) + "</button></div>" : "") +
|
|
5353
|
+
"</div></div>";
|
|
5354
|
+
}).join("");
|
|
5355
|
+
// 时间线收尾的「执行结果」卡:run 终态后的确定性摘要(成没成/耗时/执行者/
|
|
5356
|
+
// 产出文件)——不依赖模型最后一句话自觉交代(用户反馈:输"1"跑完 55 秒
|
|
5357
|
+
// 只见一句寒暄,执行结果无处可看)。
|
|
5358
|
+
const res = data && data.result;
|
|
5359
|
+
if (res) html += chatResultHTML(run, res);
|
|
5227
5360
|
const hint = $("rd-chat-hint");
|
|
5228
5361
|
if (hint) hint.textContent = active
|
|
5229
5362
|
? t("运行中:新消息会排队,本轮回答完后依次送达")
|
|
5230
5363
|
: t("已结束:发送后将自动开新一轮接着做");
|
|
5364
|
+
flow.innerHTML = html || '<div class="hint">' + esc(t("还没有对话内容")) + "</div>";
|
|
5231
5365
|
flow.scrollTop = flow.scrollHeight;
|
|
5232
5366
|
}
|
|
5233
5367
|
|
|
5234
|
-
|
|
5368
|
+
/* 执行结果卡(时间线收尾):run 终态后的确定性摘要——成没成、跑多久、谁执行
|
|
5369
|
+
* 的、产出了哪些文件,全部产品明示,不依赖模型自觉交代。失败给「查看执行步骤」
|
|
5370
|
+
* 入口;文件 chip 复用成品预览通道(artPopup)。 */
|
|
5371
|
+
function chatResultHTML(run, res) {
|
|
5372
|
+
const ok = res.status === "done";
|
|
5373
|
+
const bad = res.status === "failed";
|
|
5374
|
+
const icon = ok ? "#i-check" : "#i-x";
|
|
5375
|
+
const label = ok ? t("任务完成") : (bad ? t("任务失败") : t("已取消"));
|
|
5376
|
+
const meta = [];
|
|
5377
|
+
if (res.executor) meta.push(esc(res.executor));
|
|
5378
|
+
if (res.turns) meta.push(res.turns + " " + t("轮对话"));
|
|
5379
|
+
if (res.duration_s != null) meta.push(chatDurTxt(res.duration_s));
|
|
5380
|
+
const files = res.files || [];
|
|
5381
|
+
const chips = files.map((f) =>
|
|
5382
|
+
'<a class="file-chip chat-file-open" href="/api/runs/' + encodeURIComponent(run.id) + "/file?name=" +
|
|
5383
|
+
encodeURIComponent(f.name) + '" target="_blank" rel="noopener" title="' +
|
|
5384
|
+
esc(f.name + " · " + fmtSize(f.size)) + '" data-file-run="' + esc(run.id) + '" data-file-name="' + esc(f.name) + '" data-file-size="' + (Number(f.size) || 0) + '">' +
|
|
5385
|
+
'<i class="fx">' + esc(_fpExt(f.name).slice(0, 4) || "file") + "</i>" +
|
|
5386
|
+
'<span class="p">' + esc(f.name) + "</span><i>" + fmtSize(f.size) + "</i></a>").join("");
|
|
5387
|
+
return '<div class="chat-row">' +
|
|
5388
|
+
'<span class="chat-avatar" aria-hidden="true"><svg class="ico"><use href="' + icon + '"></use></svg></span>' +
|
|
5389
|
+
'<div class="chat-result' + (ok ? "" : bad ? " bad" : " off") + '">' +
|
|
5390
|
+
'<div class="cr-head"><svg class="ico" aria-hidden="true"><use href="' + icon + '"></use></svg>' +
|
|
5391
|
+
esc(label) + "</div>" +
|
|
5392
|
+
(meta.length ? '<div class="cr-meta">' + meta.join(" · ") + "</div>" : "") +
|
|
5393
|
+
(bad && res.error ? '<div class="cr-err">' + esc(res.error) + "</div>" : "") +
|
|
5394
|
+
'<div class="cr-files">' + (chips
|
|
5395
|
+
? '<div class="cr-files-title">' + esc(t("产出文件")) + "(" + files.length + ")</div>" +
|
|
5396
|
+
'<div class="file-chips">' + chips + "</div>"
|
|
5397
|
+
: '<div class="cr-files-title">' + esc(t("无文件产出")) + "</div>") + "</div>" +
|
|
5398
|
+
(bad ? '<button class="ghost cr-steps" onclick="rdChatNavGo(\'steps\')">' +
|
|
5399
|
+
esc(t("查看执行步骤")) + "</button>" : "") +
|
|
5400
|
+
"</div></div>";
|
|
5401
|
+
}
|
|
5402
|
+
|
|
5403
|
+
function chatDurTxt(s) {
|
|
5404
|
+
s = Number(s) || 0;
|
|
5405
|
+
if (s < 60) return s + " " + t("秒");
|
|
5406
|
+
const m = Math.floor(s / 60), r = s % 60;
|
|
5407
|
+
return m + " " + t("分") + (r ? " " + r + " " + t("秒") : "");
|
|
5408
|
+
}
|
|
5409
|
+
|
|
5410
|
+
/* 代码/编排任务的确定性结果摘要:报告是长文,步骤是过程,用户还需要一眼
|
|
5411
|
+
* 知道这次到底成没成、做了多少步、用了多久,以及失败原因。这个卡片不依赖
|
|
5412
|
+
* 智能体是否在最后一句话里主动总结;直连任务也能在「成果」页签看到同一口径。 */
|
|
5413
|
+
function runDurationSeconds(run) {
|
|
5414
|
+
if (!run) return null;
|
|
5415
|
+
if (Number.isFinite(Number(run.duration_s))) return Number(run.duration_s);
|
|
5416
|
+
const steps = (run.steps || []).filter((s) => Number.isFinite(Number(s.duration_s)));
|
|
5417
|
+
if (steps.length) return steps.reduce((a, s) => a + Number(s.duration_s || 0), 0);
|
|
5418
|
+
const a = Date.parse(String(run.started_at || "").replace(" ", "T"));
|
|
5419
|
+
const b = Date.parse(String(run.ended_at || "").replace(" ", "T"));
|
|
5420
|
+
return Number.isFinite(a) && Number.isFinite(b) && b >= a ? (b - a) / 1000 : null;
|
|
5421
|
+
}
|
|
5422
|
+
|
|
5423
|
+
function runOutcomeSummary(run) {
|
|
5424
|
+
if (!run) return "";
|
|
5425
|
+
const last = (run.steps || []).slice().reverse().find((s) => s.output || s.summary);
|
|
5426
|
+
const raw = run.summary || (last && (last.output || last.summary)) || run.error || "";
|
|
5427
|
+
return String(raw).replace(/\s+/g, " ").trim().slice(0, 520);
|
|
5428
|
+
}
|
|
5429
|
+
|
|
5430
|
+
function renderRunOutcome(run, runs) {
|
|
5431
|
+
const box = $("rd-outcome");
|
|
5432
|
+
if (!box) return;
|
|
5433
|
+
if (!run) { box.classList.add("hidden"); box.innerHTML = ""; return; }
|
|
5434
|
+
const all = runs && runs.length ? runs : [run];
|
|
5435
|
+
const steps = (run.steps || []);
|
|
5436
|
+
const total = all.reduce((n, r) => n + ((r.steps || []).length), 0);
|
|
5437
|
+
const settled = all.reduce((n, r) => n + (r.steps || []).filter((s) =>
|
|
5438
|
+
!["running", "queued"].includes(String(s.status || ""))).length, 0);
|
|
5439
|
+
const status = String(run.status || "");
|
|
5440
|
+
const active = status === "running" || status === "queued";
|
|
5441
|
+
const bad = status === "failed" || status === "cancelled" || status === "timeout";
|
|
5442
|
+
const label = active ? (status === "queued" ? t("排队中") : t("运行中"))
|
|
5443
|
+
: (status === "done" ? t("完成") : status === "failed" ? t("失败") : status === "cancelled" ? t("已取消") : status || t("未知状态"));
|
|
5444
|
+
const icon = status === "done" ? "#i-check" : bad ? "#i-x" : "#i-gauge";
|
|
5445
|
+
const duration = runDurationSeconds(run);
|
|
5446
|
+
const latest = steps.slice().reverse().find((s) => s.agent_label || s.agent || s.role);
|
|
5447
|
+
const route = run.route && typeof run.route === "object" ? run.route.implementer : "";
|
|
5448
|
+
// route.implementer 是路由评分说明,不是用户真正关心的 CLI 名称;优先
|
|
5449
|
+
// 展示步骤上的 agent_label,只有旧数据没有步骤执行者时才回退到路由说明。
|
|
5450
|
+
const executor = run.executor || run.implementer || (latest && (latest.agent_label || latest.agent)) || route || "";
|
|
5451
|
+
const summary = runOutcomeSummary(run);
|
|
5452
|
+
const meta = [];
|
|
5453
|
+
if (executor) meta.push('<span><b>' + esc(t("执行者")) + '</b> ' + esc(executor) + "</span>");
|
|
5454
|
+
if (total) meta.push('<span><b>' + esc(t("步骤")) + '</b> ' + settled + "/" + total + "</span>");
|
|
5455
|
+
if (duration != null) meta.push('<span><b>' + esc(t("耗时")) + '</b> ' + esc(chatDurTxt(duration < 1 ? Number(duration.toFixed(1)) : Math.round(duration))) + "</span>");
|
|
5456
|
+
return (box.classList.remove("hidden"), box.innerHTML =
|
|
5457
|
+
'<div class="outcome-top">' +
|
|
5458
|
+
'<div class="outcome-title ' + (bad ? "bad" : active ? "live" : "ok") + '">' +
|
|
5459
|
+
'<svg class="ico" aria-hidden="true"><use href="' + icon + '"></use></svg>' + esc(t("执行结果")) +
|
|
5460
|
+
'<span class="outcome-status">' + esc(label) + "</span></div>" +
|
|
5461
|
+
(meta.length ? '<div class="outcome-meta">' + meta.join("") + "</div>" : "") +
|
|
5462
|
+
"</div>" +
|
|
5463
|
+
(summary ? '<div class="outcome-summary">' + esc(summary) + "</div>" : "") +
|
|
5464
|
+
(run.error ? '<div class="outcome-error">' + esc(run.error) + "</div>" : "") +
|
|
5465
|
+
(total ? '<div class="outcome-actions"><button class="ghost" type="button" onclick="rdChatNavGo(\'steps\')">' + esc(t("查看执行步骤")) + "</button></div>" : "")
|
|
5466
|
+
);
|
|
5467
|
+
}
|
|
5468
|
+
|
|
5469
|
+
/* 任务详情首屏概览:把“这次任务做了什么、做到哪、最近发生了什么”放在
|
|
5470
|
+
* 蜂巢上方。直连任务仍以对话为主,由 chat-mode 隐藏概览,避免它抢走输入空间。 */
|
|
5471
|
+
function renderDetailOverview(run, runs, task) {
|
|
5472
|
+
const box = $("rd-overview");
|
|
5473
|
+
if (!box || !run) return;
|
|
5474
|
+
if (chatEngineIsDirect(run)) {
|
|
5475
|
+
box.classList.add("hidden");
|
|
5476
|
+
box.innerHTML = "";
|
|
5477
|
+
return;
|
|
5478
|
+
}
|
|
5479
|
+
const all = runs && runs.length ? runs : [run];
|
|
5480
|
+
const steps = (run.steps || []);
|
|
5481
|
+
const total = all.reduce((n, r) => n + ((r.steps || []).length), 0);
|
|
5482
|
+
const settled = all.reduce((n, r) => n + (r.steps || []).filter((s) =>
|
|
5483
|
+
!["running", "queued"].includes(String(s.status || ""))).length, 0);
|
|
5484
|
+
const pct = total ? Math.max(0, Math.min(100, Math.round((settled / total) * 100))) : 0;
|
|
5485
|
+
const status = String(run.status || "");
|
|
5486
|
+
const active = status === "running" || status === "queued";
|
|
5487
|
+
const bad = status === "failed" || status === "cancelled" || status === "timeout";
|
|
5488
|
+
const statusText = active ? (status === "queued" ? t("排队中") : t("运行中"))
|
|
5489
|
+
: status === "done" ? t("完成") : status === "failed" ? t("失败")
|
|
5490
|
+
: status === "cancelled" ? t("已取消") : status || t("未知状态");
|
|
5491
|
+
const last = steps.slice().reverse().find((s) => s.summary || s.output || s.note) || steps[steps.length - 1];
|
|
5492
|
+
const lastText = last ? String(last.summary || last.output || last.note || "").replace(/\s+/g, " ").trim().slice(0, 240) : "";
|
|
5493
|
+
const executor = run.executor || run.implementer || (last && (last.agent_label || last.agent)) || "";
|
|
5494
|
+
const duration = runDurationSeconds(run);
|
|
5495
|
+
const goal = (task && task.goal) || run.goal || run.title || "";
|
|
5496
|
+
const actions = [];
|
|
5497
|
+
if (total) actions.push('<button type="button" class="ghost" data-overview-tab="steps">' + esc(t("查看步骤")) + "</button>");
|
|
5498
|
+
if (last && last.log) actions.push('<button type="button" class="ghost" data-overview-run="' + esc(run.id) + '" data-overview-log="' + esc(last.log) + '">' + esc(t("打开 CLI 日志")) + "</button>");
|
|
5499
|
+
if (status === "done" || run.report) actions.push('<button type="button" class="ghost" data-overview-tab="result">' + esc(t("查看成果")) + "</button>");
|
|
5500
|
+
box.innerHTML =
|
|
5501
|
+
'<div class="rd-overview-main">' +
|
|
5502
|
+
'<div class="rd-overview-kicker">' + esc(t("任务概览")) + '</div>' +
|
|
5503
|
+
'<div class="rd-overview-title" title="' + esc(run.title || "") + '">' + esc(run.title || "") + '</div>' +
|
|
5504
|
+
(goal ? '<div class="rd-overview-goal" title="' + esc(goal) + '">' + esc(goal) + '</div>' : "") +
|
|
5505
|
+
'</div>' +
|
|
5506
|
+
'<div class="rd-overview-state">' +
|
|
5507
|
+
'<span class="state-label">' + esc(t("当前状态")) + '</span>' +
|
|
5508
|
+
'<strong class="state-value ' + (bad ? "bad" : status === "done" ? "ok" : "") + '">' + esc(statusText) + '</strong>' +
|
|
5509
|
+
'</div>' +
|
|
5510
|
+
'<div class="rd-overview-progress" aria-label="' + esc(pct + "%") + '"><i style="width:' + pct + '%"></i></div>' +
|
|
5511
|
+
'<div class="rd-overview-facts">' +
|
|
5512
|
+
'<span><b>' + settled + "/" + total + '</b> ' + esc(t("步骤已完成")) + '</span>' +
|
|
5513
|
+
(executor ? '<span>' + esc(t("执行者")) + ' <b>' + esc(executor) + '</b></span>' : "") +
|
|
5514
|
+
(duration != null ? '<span>' + esc(t("耗时")) + ' <b>' + esc(chatDurTxt(duration < 1 ? Number(duration.toFixed(1)) : Math.round(duration))) + '</b></span>' : "") +
|
|
5515
|
+
'</div>' +
|
|
5516
|
+
(lastText ? '<div class="rd-overview-last"><b>' + esc(t("最近一步")) + '</b><span>' + esc(lastText) + '</span></div>' : "") +
|
|
5517
|
+
(actions.length ? '<div class="rd-overview-actions">' + actions.join("") + '</div>' : "");
|
|
5518
|
+
box.classList.remove("hidden");
|
|
5519
|
+
}
|
|
5520
|
+
|
|
5521
|
+
function drawChatAtts() {
|
|
5235
5522
|
const box = $("rd-chat-att-list");
|
|
5236
5523
|
if (!box) return;
|
|
5237
5524
|
// 点名字开预览(待提交区原文走 /api/attachments/<id>),× 才是移除
|
|
@@ -5346,8 +5633,16 @@ function bindChat() {
|
|
|
5346
5633
|
// 附件胶囊点击预览:委托绑在静态容器上——时间线轮询整块重建 innerHTML、
|
|
5347
5634
|
// 输入条胶囊增删重画,都不丢监听。时间线胶囊走已落盘文件,输入条胶囊
|
|
5348
5635
|
// (× 移除按钮除外)走待提交区
|
|
5349
|
-
$("rd-chat-flow").addEventListener("click", (e) => {
|
|
5350
|
-
const
|
|
5636
|
+
$("rd-chat-flow").addEventListener("click", (e) => {
|
|
5637
|
+
const logBtn = e.target.closest("[data-chat-log-run][data-chat-log-rel]");
|
|
5638
|
+
if (logBtn) { e.stopPropagation(); toggleLog(logBtn.dataset.chatLogRun, logBtn.dataset.chatLogRel); return; }
|
|
5639
|
+
const file = e.target.closest(".chat-file-open[data-file-run]");
|
|
5640
|
+
if (file) {
|
|
5641
|
+
e.preventDefault();
|
|
5642
|
+
artPopup(file.dataset.fileRun, file.dataset.fileName || "", Number(file.dataset.fileSize) || 0);
|
|
5643
|
+
return;
|
|
5644
|
+
}
|
|
5645
|
+
const chip = e.target.closest(".att-open");
|
|
5351
5646
|
if (chip) window.chatAttPopup(chip.dataset.att);
|
|
5352
5647
|
});
|
|
5353
5648
|
const attBox = $("rd-chat-att-list");
|
|
@@ -7356,7 +7651,8 @@ async function suApply() {
|
|
|
7356
7651
|
clearInterval(SU_TIMER);
|
|
7357
7652
|
SU_TIMER = setInterval(async () => {
|
|
7358
7653
|
try {
|
|
7359
|
-
const
|
|
7654
|
+
const d = await api("/api/runs/" + encodeURIComponent(runId));
|
|
7655
|
+
const run = d && d.run; // /api/runs/<id> 返回 {run:{…}} 信封,别读外壳
|
|
7360
7656
|
if (run && run.status && run.status !== "running" && run.status !== "queued") {
|
|
7361
7657
|
clearInterval(SU_TIMER); SU_TIMER = null;
|
|
7362
7658
|
$("su-apply").disabled = false;
|
|
@@ -7688,18 +7984,31 @@ function usageMultilineSvg(byDay, byDayModel) {
|
|
|
7688
7984
|
"<title>" + esc(days[i] + "\n" + s.name + " " + fmtTok(s.vals[i]) + t(" tokens")) + "</title></circle>";
|
|
7689
7985
|
});
|
|
7690
7986
|
});
|
|
7691
|
-
const grid = [0.25, 0.5, 0.75].map((f) =>
|
|
7692
|
-
'<line class="uc-grid" x1="' + padL + '" y1="' + (padT + ih * f).toFixed(1) +
|
|
7693
|
-
'" x2="' + (W - padR) + '" y2="' + (padT + ih * f).toFixed(1) + '"/>').join("") +
|
|
7694
|
-
'<line class="uc-base" x1="' + padL + '" y1="' + (padT + ih) + '" x2="' + (W - padR) + '" y2="' + (padT + ih) + '"/>';
|
|
7987
|
+
const grid = [0.25, 0.5, 0.75].map((f) =>
|
|
7988
|
+
'<line class="uc-grid" x1="' + padL + '" y1="' + (padT + ih * f).toFixed(1) +
|
|
7989
|
+
'" x2="' + (W - padR) + '" y2="' + (padT + ih * f).toFixed(1) + '"/>').join("") +
|
|
7990
|
+
'<line class="uc-base" x1="' + padL + '" y1="' + (padT + ih) + '" x2="' + (W - padR) + '" y2="' + (padT + ih) + '"/>';
|
|
7991
|
+
// 底层活动量柱保留每日总量轮廓;折线仍表达各模型趋势,双层信息比单一曲线
|
|
7992
|
+
// 更容易看出“哪天整体有调用、哪天只是某个模型占比变化”。
|
|
7993
|
+
const dailyTotals = days.map((_, i) => series.reduce((sum, s) => sum + (s.vals[i] || 0), 0));
|
|
7994
|
+
const dayMax = Math.max(1, ...dailyTotals);
|
|
7995
|
+
const barW = Math.max(3, (iw / n) * 0.52);
|
|
7996
|
+
const bars = dailyTotals.map((v, i) => {
|
|
7997
|
+
const h = ih * v / dayMax;
|
|
7998
|
+
const x = xOf(i) - barW / 2;
|
|
7999
|
+
const y = padT + ih - h;
|
|
8000
|
+
return '<rect class="uc-bar" x="' + x.toFixed(1) + '" y="' + y.toFixed(1) +
|
|
8001
|
+
'" width="' + barW.toFixed(1) + '" height="' + Math.max(0.5, h).toFixed(1) +
|
|
8002
|
+
'" rx="2"><title>' + esc(days[i] + " · " + fmtTok(v) + t(" tokens")) + "</title></rect>";
|
|
8003
|
+
}).join("");
|
|
7695
8004
|
const legend = '<div class="um-legend">' +
|
|
7696
8005
|
series.map((s, si) =>
|
|
7697
8006
|
'<span><i style="background:' + usageColor(si) + '"></i>' +
|
|
7698
8007
|
esc(s.name.length > 22 ? s.name.slice(0, 21) + "…" : s.name) + "</span>").join("") +
|
|
7699
8008
|
'<span class="um-peak">' + esc(t("峰值 ") + fmtTok(max)) + "</span></div>";
|
|
7700
|
-
return legend +
|
|
7701
|
-
'<svg class="usage-svg" viewBox="0 0 ' + W + " " + H + '" role="img" preserveAspectRatio="xMidYMid meet">' +
|
|
7702
|
-
grid + body + labels + "</svg>";
|
|
8009
|
+
return legend +
|
|
8010
|
+
'<svg class="usage-svg" viewBox="0 0 ' + W + " " + H + '" role="img" preserveAspectRatio="xMidYMid meet">' +
|
|
8011
|
+
grid + bars + body + labels + "</svg>";
|
|
7703
8012
|
}
|
|
7704
8013
|
|
|
7705
8014
|
/* Token 活动热力图:GitHub 贡献图风格。窗口自适应——覆盖全部活跃历史(封顶
|
|
@@ -7930,9 +8239,10 @@ function renderUsage() {
|
|
|
7930
8239
|
// 不提示的话「输入 0 · 输出 0」会被误读成统计坏了
|
|
7931
8240
|
const noBreakdown = tot.tokens > 0 && !tot.input && !tot.output && !tot.cached;
|
|
7932
8241
|
$("usage-kpis").innerHTML = [
|
|
7933
|
-
kpiCard(t("累计 Token 数"), fmtTok(tot.tokens),
|
|
7934
|
-
t("调用 ") + fmtTok(tot.calls) + t(" 次 · 成功率 ") +
|
|
7935
|
-
(tot.calls ? Math.round(tot.ok * 100 / tot.calls) : 0) + "% · " +
|
|
8242
|
+
kpiCard(t("累计 Token 数"), fmtTok(tot.tokens),
|
|
8243
|
+
t("调用 ") + fmtTok(tot.calls) + t(" 次 · 成功率 ") +
|
|
8244
|
+
(tot.calls ? Math.round(tot.ok * 100 / tot.calls) : 0) + "% · " +
|
|
8245
|
+
t("失败 ") + fmtTok(tot.failed || 0) + t(" · ") + fmtUsd(tot.cost_usd),
|
|
7936
8246
|
true, "i-sigma"),
|
|
7937
8247
|
kpiCard(t("峰值 Token 数"), fmtTok(tot.peak_tokens),
|
|
7938
8248
|
t("单日最高 · 日均 ") + fmtTok(activeDays ? tot.tokens / activeDays : 0), false, "i-gauge"),
|
|
@@ -8206,9 +8516,47 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
8206
8516
|
b.addEventListener("click", () => setUsageHeatMode(b.dataset.heat)));
|
|
8207
8517
|
$("btn-back").addEventListener("click", closeRun);
|
|
8208
8518
|
$("btn-cancel").addEventListener("click", cancelRun);
|
|
8209
|
-
bindDirector();
|
|
8210
|
-
bindChat();
|
|
8211
|
-
|
|
8519
|
+
bindDirector();
|
|
8520
|
+
bindChat();
|
|
8521
|
+
const bindArtifactClicks = (root) => {
|
|
8522
|
+
if (!root) return;
|
|
8523
|
+
root.addEventListener("click", (e) => {
|
|
8524
|
+
const file = e.target.closest("[data-file-run][data-file-name]");
|
|
8525
|
+
if (!file) return;
|
|
8526
|
+
e.preventDefault();
|
|
8527
|
+
artPopup(file.dataset.fileRun, file.dataset.fileName || "", Number(file.dataset.fileSize) || 0);
|
|
8528
|
+
});
|
|
8529
|
+
};
|
|
8530
|
+
bindArtifactClicks($("rd-arts"));
|
|
8531
|
+
bindArtifactClicks($("insp-artifacts"));
|
|
8532
|
+
// 动态详情区统一用 data 属性委托,日志路径/文件名不再拼进 inline JS。
|
|
8533
|
+
const detailOverview = $("rd-overview");
|
|
8534
|
+
if (detailOverview) detailOverview.addEventListener("click", (e) => {
|
|
8535
|
+
const log = e.target.closest("[data-overview-log]");
|
|
8536
|
+
if (log) { toggleLog(log.dataset.overviewRun, log.dataset.overviewLog); return; }
|
|
8537
|
+
const tab = e.target.closest("[data-overview-tab]");
|
|
8538
|
+
if (tab) { S.rdTab = tab.dataset.overviewTab; S.rdTabPin = true; applyRdTabs(); }
|
|
8539
|
+
});
|
|
8540
|
+
const stepBox = $("rd-steps");
|
|
8541
|
+
if (stepBox) stepBox.addEventListener("click", (e) => {
|
|
8542
|
+
const btn = e.target.closest("[data-log-run][data-log-rel]");
|
|
8543
|
+
const row = e.target.closest(".step[data-log]");
|
|
8544
|
+
if (btn) { e.stopPropagation(); toggleLog(btn.dataset.logRun, btn.dataset.logRel); return; }
|
|
8545
|
+
if (row && row.dataset.log) toggleLog(row.dataset.runId || S.detailRunId, row.dataset.log);
|
|
8546
|
+
});
|
|
8547
|
+
const hiveBox = $("rd-hive-cells");
|
|
8548
|
+
if (hiveBox) {
|
|
8549
|
+
hiveBox.addEventListener("click", (e) => {
|
|
8550
|
+
const cell = e.target.closest("[data-hive-run][data-hive-log]");
|
|
8551
|
+
if (cell) hiveOpenLog(cell.dataset.hiveRun, cell.dataset.hiveLog);
|
|
8552
|
+
});
|
|
8553
|
+
hiveBox.addEventListener("keydown", (e) => {
|
|
8554
|
+
if (e.key !== "Enter" && e.key !== " ") return;
|
|
8555
|
+
const cell = e.target.closest("[data-hive-run][data-hive-log]");
|
|
8556
|
+
if (cell) { e.preventDefault(); hiveOpenLog(cell.dataset.hiveRun, cell.dataset.hiveLog); }
|
|
8557
|
+
});
|
|
8558
|
+
}
|
|
8559
|
+
// 详情标签页:手点即切并钉住——状态变化触发的自动选卡不再抢用户的手选
|
|
8212
8560
|
$("rd-tabs").addEventListener("click", (e) => {
|
|
8213
8561
|
const b = e.target.closest(".rd-tab");
|
|
8214
8562
|
if (!b || b.classList.contains("hidden")) return;
|
|
@@ -8216,6 +8564,21 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
8216
8564
|
S.rdTabPin = true;
|
|
8217
8565
|
applyRdTabs();
|
|
8218
8566
|
});
|
|
8567
|
+
$("rd-tabs").addEventListener("keydown", (e) => {
|
|
8568
|
+
if (!["ArrowLeft", "ArrowRight", "Home", "End"].includes(e.key)) return;
|
|
8569
|
+
const tabs = Array.from($("rd-tabs").querySelectorAll(".rd-tab:not(.hidden)"));
|
|
8570
|
+
if (!tabs.length) return;
|
|
8571
|
+
const current = Math.max(0, tabs.indexOf(document.activeElement));
|
|
8572
|
+
let next = current;
|
|
8573
|
+
if (e.key === "ArrowRight") next = (current + 1) % tabs.length;
|
|
8574
|
+
if (e.key === "ArrowLeft") next = (current - 1 + tabs.length) % tabs.length;
|
|
8575
|
+
if (e.key === "Home") next = 0;
|
|
8576
|
+
if (e.key === "End") next = tabs.length - 1;
|
|
8577
|
+
e.preventDefault();
|
|
8578
|
+
tabs[next].focus();
|
|
8579
|
+
tabs[next].click();
|
|
8580
|
+
tabs[next].scrollIntoView({ block: "nearest", inline: "nearest" });
|
|
8581
|
+
});
|
|
8219
8582
|
// 详情子页显隐 → 容器宽度 class(:has 在部分浏览器不生效,JS 同步为准)
|
|
8220
8583
|
(function () {
|
|
8221
8584
|
const sub = document.getElementById("sub-runs");
|