codebee 0.1.49 → 0.1.50
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 +24 -0
- package/README.md +4 -150
- package/app/core/aiflavor.py +14 -0
- package/app/core/builtin_agent.py +450 -32
- package/app/core/knowledge.py +432 -405
- package/app/core/manager.py +76 -3
- package/app/core/modelhub.py +53 -5
- package/app/core/pipeline.py +158 -66
- package/app/core/preview.py +293 -0
- package/app/core/runner.py +336 -144
- package/app/core/settings.py +7 -1
- package/app/core/store.py +74 -2
- package/app/core/zentao.py +63 -13
- package/app/main.py +66 -1
- package/app/ui/app.js +397 -102
- package/app/ui/i18n.js +41 -30
- package/app/ui/index.html +45 -2
- package/app/ui/style.css +206 -26
- package/package.json +1 -1
package/app/ui/app.js
CHANGED
|
@@ -10,7 +10,7 @@ function codebeeDocumentTitle(lang) {
|
|
|
10
10
|
document.title = codebeeDocumentTitle((localStorage.getItem("orch.lang") || "zh").toLowerCase());
|
|
11
11
|
|
|
12
12
|
const $ = (id) => document.getElementById(id);
|
|
13
|
-
const S = { state: null, catalog: null, catSig: "", providers: null, bindings: null, modelsSig: "", bindSig: "", tab: "tasks", mainPage: "", /* 主栏正显示的全局页("" = 新建任务表单);左栏是任务树还是设置导航看 body.settings-mode */ 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: {}, lastPrefs: null, prefsApplied: false };
|
|
13
|
+
const S = { state: null, catalog: null, catSig: "", providers: null, bindings: null, modelsSig: "", bindSig: "", tab: "tasks", mainPage: "", /* 主栏正显示的全局页("" = 新建任务表单);左栏是任务树还是设置导航看 body.settings-mode */ 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: {}, lastPrefs: null, prefsApplied: false, sideUsage: null, sideUsageAt: 0, ovUsage: null, ovDays: undefined };
|
|
14
14
|
|
|
15
15
|
/* ---------------------------------------------------------- 任务类型(流程) */
|
|
16
16
|
async function loadFlows() {
|
|
@@ -1093,12 +1093,12 @@ async function healthOp(op) {
|
|
|
1093
1093
|
}
|
|
1094
1094
|
}
|
|
1095
1095
|
|
|
1096
|
-
/*
|
|
1096
|
+
/* 模型级禁用:绑定链里它的条目由后端自动剔除(modelhub 停用即出调度)。
|
|
1097
1097
|
只对出问题的那一格生效——厂商其它模型照常可用。 */
|
|
1098
1098
|
async function healthDisableModel(pid, model) {
|
|
1099
1099
|
if (!pid || !model) { closeModal(); return; }
|
|
1100
1100
|
const yes = await uiConfirm(
|
|
1101
|
-
t("确认禁用模型 {0}
|
|
1101
|
+
t("确认禁用模型 {0}?调度链里它的条目会自动移除,其余模型不受影响;可在模型调度页重新启用。").replace("{0}", pid + " · " + model),
|
|
1102
1102
|
{ title: t("禁用模型"), danger: true, ok: t("禁用") });
|
|
1103
1103
|
if (!yes) return;
|
|
1104
1104
|
try {
|
|
@@ -1113,8 +1113,7 @@ async function healthDisableModel(pid, model) {
|
|
|
1113
1113
|
S.modelCatalog = models.catalog || [];
|
|
1114
1114
|
S.provSig = "";
|
|
1115
1115
|
closeModal(); render(); loadOrchestrator();
|
|
1116
|
-
toast(t("已禁用模型 {0} · {1}
|
|
1117
|
-
autoRebindSoon();
|
|
1116
|
+
toast(t("已禁用模型 {0} · {1},调度链已自动移除它;绑定页可重新启用").replace("{0}", pid).replace("{1}", model));
|
|
1118
1117
|
} catch (e) {
|
|
1119
1118
|
toast(t("操作失败:") + e.message, true);
|
|
1120
1119
|
}
|
|
@@ -1123,7 +1122,7 @@ async function healthDisableModel(pid, model) {
|
|
|
1123
1122
|
async function healthDisableProvider(pid) {
|
|
1124
1123
|
if (!pid) { closeModal(); return; }
|
|
1125
1124
|
const yes = await uiConfirm(
|
|
1126
|
-
t("
|
|
1125
|
+
t("确认禁用该厂商?调度链里它的条目会自动移除,恢复后可在模型调度页重新启用。"),
|
|
1127
1126
|
{ title: t("禁用厂商"), danger: true, ok: t("禁用") });
|
|
1128
1127
|
if (!yes) return;
|
|
1129
1128
|
try {
|
|
@@ -1137,8 +1136,7 @@ async function healthDisableProvider(pid) {
|
|
|
1137
1136
|
S.providers = models.providers; S.bindings = models.bindings;
|
|
1138
1137
|
S.provSig = "";
|
|
1139
1138
|
closeModal(); render(); loadOrchestrator();
|
|
1140
|
-
toast(t("已禁用厂商 {0}
|
|
1141
|
-
autoRebindSoon();
|
|
1139
|
+
toast(t("已禁用厂商 {0}:调度链已自动移除它的条目,绑定页可重新启用").replace("{0}", pid));
|
|
1142
1140
|
} catch (e) {
|
|
1143
1141
|
toast(t("操作失败:") + e.message, true);
|
|
1144
1142
|
}
|
|
@@ -1450,7 +1448,6 @@ async function batchProvOp(op) {
|
|
|
1450
1448
|
try {
|
|
1451
1449
|
await api("/api/models/provider-op", { method: "POST",
|
|
1452
1450
|
body: JSON.stringify({ ids, op }) });
|
|
1453
|
-
if (op !== "duplicate") autoRebindSoon();
|
|
1454
1451
|
} catch (e) { toast(t("操作失败:") + e.message, true); }
|
|
1455
1452
|
S.selProvs = {};
|
|
1456
1453
|
S.modelsSig = null;
|
|
@@ -1641,7 +1638,6 @@ async function keyOpCall(pid, op, keyId, key, label) {
|
|
|
1641
1638
|
try {
|
|
1642
1639
|
await api("/api/models/key-op", { method: "POST",
|
|
1643
1640
|
body: JSON.stringify({ provider_id: pid, op, key_id: keyId, key, label }) });
|
|
1644
|
-
autoRebindSoon(); // 密钥启停/删除/解冻会改变厂商「推荐可用」判定,自动补绑一次
|
|
1645
1641
|
} catch (e) { toast(t("操作失败:") + e.message, true); }
|
|
1646
1642
|
// 操作期间轮询照跑(uiPrompt 弹框不阻塞轮询),S.modelsSig 还是旧值:必须
|
|
1647
1643
|
// 清签名强制重绘,否则 KEY 写盘成功界面也看不到。
|
|
@@ -1870,7 +1866,6 @@ async function modelOp(pid, name, op) {
|
|
|
1870
1866
|
try {
|
|
1871
1867
|
await api("/api/models/model-op", { method: "POST",
|
|
1872
1868
|
body: JSON.stringify({ provider_id: pid, name, op }) });
|
|
1873
|
-
autoRebindSoon(); // 模型停用/启用/删除会改变推荐源与死链判定,自动补绑一次
|
|
1874
1869
|
const s = modelSel(pid); // 单行操作后同步清掉该行勾选
|
|
1875
1870
|
if (s[name]) { delete modelSelSet(pid)[name]; }
|
|
1876
1871
|
} catch (e) { toast(t("操作失败:") + e.message, true); }
|
|
@@ -1937,7 +1932,6 @@ async function batchModelOp(pid, op) {
|
|
|
1937
1932
|
try {
|
|
1938
1933
|
await api("/api/models/model-op", { method: "POST",
|
|
1939
1934
|
body: JSON.stringify({ provider_id: pid, names, op }) });
|
|
1940
|
-
autoRebindSoon();
|
|
1941
1935
|
} catch (e) { toast(t("操作失败:") + e.message, true); }
|
|
1942
1936
|
clearModelSel(pid);
|
|
1943
1937
|
poll();
|
|
@@ -1956,7 +1950,6 @@ async function toggleProviderEnabled(pid, enabled) {
|
|
|
1956
1950
|
try {
|
|
1957
1951
|
await api("/api/models/provider-op", { method: "POST",
|
|
1958
1952
|
body: JSON.stringify({ ids: [pid], op: off ? "enable" : "disable" }) });
|
|
1959
|
-
autoRebindSoon();
|
|
1960
1953
|
} catch (e) { toast(t("操作失败:") + e.message, true); }
|
|
1961
1954
|
S.modelsSig = null;
|
|
1962
1955
|
poll();
|
|
@@ -1981,7 +1974,6 @@ async function refreshAllModels() {
|
|
|
1981
1974
|
btn.title = t("全部重新拉取模型列表");
|
|
1982
1975
|
}, 1500);
|
|
1983
1976
|
setTimeout(poll, 2500);
|
|
1984
|
-
setTimeout(autoRebindSoon, 8000); // 模型列表落盘后推荐源才完整,补绑一次
|
|
1985
1977
|
}
|
|
1986
1978
|
|
|
1987
1979
|
/* 手工添加模型:有的厂商列表接口调不通(或只返回部分),直接填模型名也能进列表。
|
|
@@ -2010,7 +2002,6 @@ async function addModelManual(pid) {
|
|
|
2010
2002
|
toast(t("已添加模型 ") + name);
|
|
2011
2003
|
S.modelsSig = null; // 新模型落盘后签名必变,这里主动置空确保立刻重绘
|
|
2012
2004
|
poll();
|
|
2013
|
-
setTimeout(autoRebindSoon, 4000); // 模型可用后推荐源更完整,补绑一次
|
|
2014
2005
|
} catch (e) { toast(t("添加失败:") + e.message, true); }
|
|
2015
2006
|
}
|
|
2016
2007
|
|
|
@@ -2023,7 +2014,6 @@ async function refreshProviderModels(id) {
|
|
|
2023
2014
|
else toast(t("已获取模型列表 · wire 适配测试后台进行中"));
|
|
2024
2015
|
} catch (e) { toast(t("获取失败:") + e.message, true); }
|
|
2025
2016
|
poll();
|
|
2026
|
-
setTimeout(autoRebindSoon, 4000); // 模型列表落盘后补绑一次(后台 wire 探测同步进行)
|
|
2027
2017
|
}
|
|
2028
2018
|
|
|
2029
2019
|
/* 供应商编辑卡:协议可选「自动」;显式选定时它是主协议(用于需要唯一协议的
|
|
@@ -2079,7 +2069,6 @@ async function delProvider(id) {
|
|
|
2079
2069
|
try {
|
|
2080
2070
|
await api("/api/models/provider-op", { method: "POST",
|
|
2081
2071
|
body: JSON.stringify({ ids: [id], op: "delete" }) });
|
|
2082
|
-
autoRebindSoon();
|
|
2083
2072
|
} catch (e) { toast(t("操作失败:") + e.message, true); }
|
|
2084
2073
|
if (S.selModels) delete S.selModels[id];
|
|
2085
2074
|
if (S.selProvs) delete S.selProvs[id];
|
|
@@ -2143,7 +2132,6 @@ async function doAddProvider() {
|
|
|
2143
2132
|
S.modelsSig = null;
|
|
2144
2133
|
closeModal();
|
|
2145
2134
|
poll();
|
|
2146
|
-
autoRebindSoon(); // 新增即启用:马上给空链/死链一次推荐机会
|
|
2147
2135
|
if (p && body.api_key) refreshProviderModels(p.id); // 有密钥才自动拉模型列表
|
|
2148
2136
|
} catch (e) {
|
|
2149
2137
|
res.textContent = t("保存失败:") + e.message;
|
|
@@ -2239,7 +2227,7 @@ async function doImport() {
|
|
|
2239
2227
|
btn.textContent = t("完成");
|
|
2240
2228
|
btn.disabled = false;
|
|
2241
2229
|
btn.onclick = closeModal;
|
|
2242
|
-
if (r.imported) { S.selProv = null; S.modelsSig = null; poll();
|
|
2230
|
+
if (r.imported) { S.selProv = null; S.modelsSig = null; poll(); }
|
|
2243
2231
|
} catch (e) {
|
|
2244
2232
|
$("import-result").innerHTML = '<div class="msg bad">' + t("导入失败:") + esc(e.message) + "</div>";
|
|
2245
2233
|
btn.disabled = false; btn.textContent = t("导入选中");
|
|
@@ -3358,17 +3346,22 @@ function renderRunList() {
|
|
|
3358
3346
|
"</div>";
|
|
3359
3347
|
box.innerHTML = tools + runs.map((r) => {
|
|
3360
3348
|
const can = runDeletable(r);
|
|
3349
|
+
// 行结构照参考站的「主副标题 + 右侧状态与相对时间」:标题与摘要竖排成一块,
|
|
3350
|
+
// 状态点用侧栏同款字形(staskGlyph),时间用相对档(完整时间戳挪进悬停提示)
|
|
3351
|
+
const sub = String(r.summary || r.error || (r.steps ? r.steps.length + t(" 个步骤") : "")).trim();
|
|
3361
3352
|
return '<div class="item" onclick="openRun(\'' + esc(r.id) + '\')">' +
|
|
3362
3353
|
'<div class="t">' +
|
|
3363
3354
|
'<input type="checkbox" class="rcheck"' + (S.selRuns[r.id] ? " checked" : "") + (can ? "" : " disabled") +
|
|
3364
3355
|
' title="' + (can ? t("勾选以批量删除") : t("运行中的记录不可删除,请先取消")) + '"' +
|
|
3365
3356
|
' onclick="event.stopPropagation()" onchange="toggleRunSel(\'' + esc(r.id) + '\', this.checked)">' +
|
|
3366
|
-
|
|
3367
|
-
'<
|
|
3357
|
+
'<span class="rglyph">' + staskGlyph(r.status || "") + "</span>" +
|
|
3358
|
+
'<div class="rt">' +
|
|
3359
|
+
'<span class="name">' + esc(r.title) + runKindTag(r.kind) + "</span>" +
|
|
3360
|
+
(sub ? '<span class="desc">' + esc(t(sub)) + "</span>" : "") +
|
|
3361
|
+
"</div>" +
|
|
3368
3362
|
'<span class="chip ' + esc(r.status || "") + '">' + esc(runStatusText(r)) + "</span>" +
|
|
3369
|
-
'<span class="time">' + esc(r.created_at) + "</span>" +
|
|
3370
|
-
'<button class="danger small" title="' + t("删除该记录") + '" onclick="event.stopPropagation(); deleteRun(\'' + esc(r.id) + '\')">' + t("删除") + '</button></div>'
|
|
3371
|
-
'<div class="desc">' + esc(t(r.summary || r.error || (r.steps ? r.steps.length + t(" 个步骤") : ""))) + "</div></div>";
|
|
3363
|
+
'<span class="time" title="' + esc(r.created_at) + '">' + esc(relTime(r.created_at)) + "</span>" +
|
|
3364
|
+
'<button class="danger small" title="' + t("删除该记录") + '" onclick="event.stopPropagation(); deleteRun(\'' + esc(r.id) + '\')">' + t("删除") + '</button></div></div>';
|
|
3372
3365
|
}).join("");
|
|
3373
3366
|
}
|
|
3374
3367
|
|
|
@@ -3655,8 +3648,7 @@ function showDetailInMain() {
|
|
|
3655
3648
|
document.querySelectorAll("#page-settings .subpage").forEach((d) => d.classList.toggle("hidden", d.id !== "sub-runs"));
|
|
3656
3649
|
document.querySelectorAll(".set-item").forEach((b) => b.classList.remove("active"));
|
|
3657
3650
|
document.body.classList.remove("settings-mode");
|
|
3658
|
-
|
|
3659
|
-
if (title) title.textContent = t("运行详情");
|
|
3651
|
+
renderPageCrumb();
|
|
3660
3652
|
collapseDrawerIfMobile();
|
|
3661
3653
|
syncInspectorVis(); // 从设置子页点进运行详情:任务上下文,检查器跟着回来
|
|
3662
3654
|
}
|
|
@@ -3923,6 +3915,7 @@ function drawTaskDetail(key, runs) {
|
|
|
3923
3915
|
};
|
|
3924
3916
|
drawReport();
|
|
3925
3917
|
loadArtifacts(latest.id); // 成品文件双入口同源:主栏「成果」分区 + 检查器(列表上下文)
|
|
3918
|
+
renderPreview(latest.id); // 网页成品预览:入口页 + 可运行 iframe(无成品则整块隐藏)
|
|
3926
3919
|
// 任务级详情:git 面板吃任务级 side(实时/最新快照),side 未到先以最新 run 快照落位
|
|
3927
3920
|
const tk = ((S.state || {}).tasks || []).find((x) => x.id === key);
|
|
3928
3921
|
S.lastRunTask = tk || null;
|
|
@@ -3996,6 +3989,7 @@ async function clearRuns() {
|
|
|
3996
3989
|
async function openRun(id, pinTab) {
|
|
3997
3990
|
S.detailRunId = id;
|
|
3998
3991
|
S.detailTaskKey = null;
|
|
3992
|
+
renderPageCrumb(); // 标题「运行详情」;必须在赋值后画——showDetailInMain 先于本函数跑,那边画不到
|
|
3999
3993
|
rdTabReset(pinTab || null); // sideOpenRun 带步骤号时钉住步骤分区
|
|
4000
3994
|
document.querySelector("#sub-runs .panel:first-child").classList.add("hidden");
|
|
4001
3995
|
$("run-detail").classList.remove("hidden");
|
|
@@ -4018,16 +4012,19 @@ function closeRun() {
|
|
|
4018
4012
|
if (document.body.classList.contains("settings-mode") || S.mainPage === "runs") {
|
|
4019
4013
|
// 回运行列表:设置导航里点进来的,或主栏本就停在运行记录页(快捷导航/概览的入口)
|
|
4020
4014
|
document.querySelector("#sub-runs .panel:first-child").classList.remove("hidden");
|
|
4015
|
+
renderPageCrumb(); // 详情关了,标题从「运行详情」回到当前页名
|
|
4021
4016
|
} else if (S.mainPage) {
|
|
4022
4017
|
// 主栏停在别的快捷导航页(概览/自动化):返回点开详情前那一页,左栏任务树不动
|
|
4023
4018
|
switchTab(S.mainPage, "main");
|
|
4024
4019
|
} else {
|
|
4025
|
-
//
|
|
4020
|
+
// 从主视图(侧栏点任务行)进来的详情:返回直接回任务页,不露出运行列表。
|
|
4021
|
+
// S.tab 还停在 "runs"(showDetailInMain 设的),视图既然回到任务页就一并归位,
|
|
4022
|
+
// 否则标题会拿 "runs" 的页名显示成「运行记录」
|
|
4023
|
+
S.tab = "tasks";
|
|
4026
4024
|
document.querySelectorAll("#page-settings .subpage").forEach((d) => d.classList.toggle("hidden", d.id !== "sub-tasks"));
|
|
4027
4025
|
document.querySelectorAll(".set-item").forEach((b) => b.classList.toggle("active", b.dataset.sub === "tasks"));
|
|
4028
4026
|
document.querySelectorAll(".qitem").forEach((b) => b.classList.remove("active"));
|
|
4029
|
-
|
|
4030
|
-
if (title) title.textContent = tabTitle("tasks");
|
|
4027
|
+
renderPageCrumb();
|
|
4031
4028
|
}
|
|
4032
4029
|
syncInspectorVis(); // 离开详情:回设置运行列表时检查器按各上下文规则重新落位
|
|
4033
4030
|
}
|
|
@@ -4064,13 +4061,14 @@ function rdTabAvail() {
|
|
|
4064
4061
|
git: !$("rd-git").classList.contains("hidden"),
|
|
4065
4062
|
bible: !$("rd-bible").classList.contains("hidden"),
|
|
4066
4063
|
bookmeta: !$("rd-bookmeta").classList.contains("hidden"),
|
|
4064
|
+
preview: !$("rd-preview").classList.contains("hidden"),
|
|
4067
4065
|
};
|
|
4068
4066
|
}
|
|
4069
4067
|
|
|
4070
4068
|
function applyRdTabs() {
|
|
4071
4069
|
const avail = rdTabAvail();
|
|
4072
4070
|
if (!S.rdTab || !avail[S.rdTab]) {
|
|
4073
|
-
S.rdTab = ["chat", "hive", "steps", "result", "git", "bible", "bookmeta"]
|
|
4071
|
+
S.rdTab = ["chat", "hive", "steps", "result", "preview", "git", "bible", "bookmeta"]
|
|
4074
4072
|
.find((k) => avail[k]) || "steps";
|
|
4075
4073
|
}
|
|
4076
4074
|
document.querySelectorAll("#rd-tabs .rd-tab").forEach((b) => {
|
|
@@ -4110,10 +4108,10 @@ function renderChatNav() {
|
|
|
4110
4108
|
if (mainEl) mainEl.classList.toggle("chat-fill", direct);
|
|
4111
4109
|
if (!direct) { nav.classList.add("hidden"); nav.innerHTML = ""; return; }
|
|
4112
4110
|
const labels = { hive: t("蜂巢"), steps: t("步骤"), result: t("成果"),
|
|
4113
|
-
git: "Git", bible: t("圣经"), bookmeta: t("作品信息") };
|
|
4111
|
+
preview: t("预览"), git: "Git", bible: t("圣经"), bookmeta: t("作品信息") };
|
|
4114
4112
|
const avail = rdTabAvail();
|
|
4115
4113
|
let pills = "";
|
|
4116
|
-
for (const k of ["hive", "steps", "result", "git", "bible", "bookmeta"]) {
|
|
4114
|
+
for (const k of ["preview", "hive", "steps", "result", "git", "bible", "bookmeta"]) {
|
|
4117
4115
|
if (!avail[k]) continue;
|
|
4118
4116
|
// 徽章直接镜像隐藏页签上的(蜂巢在岗数/步骤数/待裁决/成果数),不另设状态源
|
|
4119
4117
|
const badge = document.querySelector('#rd-tabs .rd-tab[data-tab="' + k + '"] .rd-badge');
|
|
@@ -4155,16 +4153,18 @@ function rdTabBadges() {
|
|
|
4155
4153
|
set("git", c.gitState === "isolated" ? t("待裁决") : "", "verdict");
|
|
4156
4154
|
}
|
|
4157
4155
|
|
|
4158
|
-
/* ctx
|
|
4159
|
-
*
|
|
4156
|
+
/* ctx 可省略:省略时沿用上次的 _rdCtx 做自动选卡判断(renderHive/renderGitPanel/
|
|
4157
|
+
* renderPreview 等收尾调用——预览可用性是异步翻过来的,翻过来这次必须重选卡)。 */
|
|
4160
4158
|
function rdTabsSync(ctx) {
|
|
4161
4159
|
if (ctx) S._rdCtx = ctx;
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
// chat 从不可用变可用时必须触发一次重新选卡(否则落在步骤/蜂巢不跳对话)
|
|
4160
|
+
const c = ctx || S._rdCtx || {};
|
|
4161
|
+
if (!S.rdTabPin) {
|
|
4165
4162
|
const chatAvail = $("rd-chat") && !$("rd-chat").classList.contains("hidden");
|
|
4166
|
-
|
|
4167
|
-
|
|
4163
|
+
// 预览可用性入签名:网页成品是跑完才落盘的,从不可用变可用时要重选一次卡
|
|
4164
|
+
const pvAvail = $("rd-preview") && !$("rd-preview").classList.contains("hidden");
|
|
4165
|
+
const sig = (S.detailTaskKey || S.detailRunId || "") + "|" + (c.status || "") +
|
|
4166
|
+
"|" + (c.gitState || "") + "|" + (c.running ? 1 : 0) + "|" + (chatAvail ? 1 : 0) +
|
|
4167
|
+
"|" + (pvAvail ? 1 : 0);
|
|
4168
4168
|
if (S.rdTabSig !== sig) {
|
|
4169
4169
|
S.rdTabSig = sig;
|
|
4170
4170
|
const avail = rdTabAvail();
|
|
@@ -4172,13 +4172,17 @@ function rdTabsSync(ctx) {
|
|
|
4172
4172
|
// 直连任务的主问题是「继续聊什么、上一轮回答是什么」——默认把对话
|
|
4173
4173
|
// 放在第一视线;编排/代码任务才默认落蜂巢,先看阶段与在岗步骤。
|
|
4174
4174
|
// 没跑到终态时成果分区是空的,避免打开详情先看到白板。
|
|
4175
|
-
const finishing = !
|
|
4175
|
+
const finishing = !c.running && ["done", "failed", "cancelled", "timeout"].includes(c.status);
|
|
4176
4176
|
S.rdTab = (direct && avail.chat ? "chat" : null)
|
|
4177
|
+
// 跑出了能打开的网页成品:第一视线给「预览」——这正是用户做完一个
|
|
4178
|
+
// 前端任务最想看到的(借鉴对话式编程产品:跑完先看东西跑起来)。
|
|
4179
|
+
// 直连任务的对话、待裁决的版本仍排在它前面:那两个是"要用户说话/动手"。
|
|
4180
|
+
|| (finishing && avail.preview ? "preview" : null)
|
|
4177
4181
|
|| (avail.hive ? "hive" : null)
|
|
4178
4182
|
|| (avail.chat ? "chat" : null)
|
|
4179
|
-
|| (
|
|
4180
|
-
: (
|
|
4181
|
-
: (
|
|
4183
|
+
|| (c.running ? (avail.hive ? "hive" : "steps")
|
|
4184
|
+
: (c.gitState === "isolated" && avail.git ? "git"
|
|
4185
|
+
: (c.hasResult && finishing ? "result" : "steps")));
|
|
4182
4186
|
}
|
|
4183
4187
|
}
|
|
4184
4188
|
applyRdTabs();
|
|
@@ -4203,6 +4207,7 @@ function rdTabReset(pin) {
|
|
|
4203
4207
|
S.rdTabSig = "";
|
|
4204
4208
|
S.rdTabPin = !!pin;
|
|
4205
4209
|
S._rdCtx = {};
|
|
4210
|
+
pvReset(); // 预览也随详情目标清空:别家的 iframe/页签绝不带到新详情
|
|
4206
4211
|
syncChatLogSpace(false);
|
|
4207
4212
|
setRdMetaOpen(false);
|
|
4208
4213
|
}
|
|
@@ -4297,6 +4302,9 @@ async function renderRunDetail() {
|
|
|
4297
4302
|
: esc(t("本次运行没有生成报告") + (st ? t("(状态:") + st + t(")") : "") + t(";各步骤日志在「步骤」页签"))) +
|
|
4298
4303
|
"</div>";
|
|
4299
4304
|
}
|
|
4305
|
+
// 网页成品预览:与成果同源双入口都走这里;无成功报告时也要重新判定可用性
|
|
4306
|
+
// (换到没有网页成品的 run 必须把「预览」页签收起来,别留着上一家的 iframe)
|
|
4307
|
+
renderPreview(id);
|
|
4300
4308
|
// 成品文件双入口同源:主栏「成果」分区 + 检查器成品 TAB(列表上下文),
|
|
4301
4309
|
// 详情上下文检查器让位后主栏是唯一可见面
|
|
4302
4310
|
S.lastRunTask = rcTask || null;
|
|
@@ -5890,6 +5898,131 @@ async function loadArtifacts(runId) {
|
|
|
5890
5898
|
mainBox.innerHTML = head + '<div class="file-chips">' + chips + "</div>"; }
|
|
5891
5899
|
}
|
|
5892
5900
|
|
|
5901
|
+
/* ---------------- 网页成品预览(借鉴对话式编程产品的预览窗) ----------------
|
|
5902
|
+
* 代码任务跑完,用户要的是「看到东西跑起来」,而不是逐个点开文件读代码。
|
|
5903
|
+
* 左「运行」= 入口 HTML 的 iframe(后端 /preview/<run>/<令牌>/ 只读挂载工作
|
|
5904
|
+
* 目录,令牌走路径段让 style.css / script.js 这些相对子资源也带上);
|
|
5905
|
+
* 右各文件页签 = 源码视图(复用 codeBlockHTML:行号+高亮,与成果预览同款)。
|
|
5906
|
+
* 「刷新」重挂 iframe(改完文件不用退出详情页),「打开新窗口」给真全屏。
|
|
5907
|
+
* 页签在无网页成品时整体隐藏(见 rdTabAvail),不打扰小说/普通任务。 */
|
|
5908
|
+
let pvRunId = null; // 当前预览归属的 run(换 run 必须重挑入口与页签)
|
|
5909
|
+
let pvApp = null; // 后端 /api/runs/<id>/preview 的返回(入口 + 文件清单 + base)
|
|
5910
|
+
let pvView = "run"; // "run" = 运行视图;否则是文件名
|
|
5911
|
+
let pvCodeSig = ""; // 代码视图去抖签名(轮询重画不重取同一个文件)
|
|
5912
|
+
|
|
5913
|
+
function pvReset() {
|
|
5914
|
+
pvRunId = null; pvApp = null; pvView = "run"; pvCodeSig = "";
|
|
5915
|
+
}
|
|
5916
|
+
|
|
5917
|
+
/* 预览 iframe 的地址:base 已带令牌与尾部斜杠,相对路径交给 <base href> 解析 */
|
|
5918
|
+
function pvRunUrl() {
|
|
5919
|
+
if (!pvApp || !pvApp.base) return "";
|
|
5920
|
+
return urlAuth(pvApp.base + (pvApp.entry || ""));
|
|
5921
|
+
}
|
|
5922
|
+
|
|
5923
|
+
function pvTabsHTML() {
|
|
5924
|
+
const f = (pvApp && pvApp.files) || [];
|
|
5925
|
+
const mk = (key, label, title) =>
|
|
5926
|
+
'<button class="pv-tab' + (pvView === key ? " on" : "") + '" role="tab" ' +
|
|
5927
|
+
'aria-selected="' + (pvView === key ? "true" : "false") + '" data-pv="' + esc(key) + '" ' +
|
|
5928
|
+
'title="' + esc(title || label) + '">' + esc(label) + "</button>";
|
|
5929
|
+
let h = mk("run", t("运行视图"), t("实时运行入口页面"));
|
|
5930
|
+
for (const it of f) h += mk(it.name, it.name.split("/").pop(), it.name);
|
|
5931
|
+
return h;
|
|
5932
|
+
}
|
|
5933
|
+
|
|
5934
|
+
function pvBodyHTML() {
|
|
5935
|
+
if (!pvApp || !pvApp.ok) return "";
|
|
5936
|
+
if (pvView === "run") {
|
|
5937
|
+
// 每次重挂 iframe 都换 src 尾参,绕开浏览器缓存:用户点「刷新」就是要看新代码
|
|
5938
|
+
const src = pvRunUrl();
|
|
5939
|
+
return '<iframe class="pv-frame" id="rd-pv-frame" src="' + esc(src) +
|
|
5940
|
+
'" title="' + esc(t("运行预览")) + '" sandbox="allow-scripts allow-same-origin allow-forms allow-modals allow-popups allow-downloads"></iframe>';
|
|
5941
|
+
}
|
|
5942
|
+
return '<div class="pv-code"><div class="fp-vp" id="rd-pv-code">' +
|
|
5943
|
+
'<div class="fp-hint">' + esc(t("正在读取文件…")) + "</div></div></div>";
|
|
5944
|
+
}
|
|
5945
|
+
|
|
5946
|
+
async function pvLoadCode(name) {
|
|
5947
|
+
const el = $("rd-pv-code");
|
|
5948
|
+
if (!el) return;
|
|
5949
|
+
const url = "/api/runs/" + encodeURIComponent(pvRunId) + "/file?name=" + encodeURIComponent(name);
|
|
5950
|
+
try {
|
|
5951
|
+
const r = await fetch(urlAuth(url), { headers: authHeaders() });
|
|
5952
|
+
if (!r.ok) throw new Error("HTTP " + r.status);
|
|
5953
|
+
const text = await r.text();
|
|
5954
|
+
if (!el.isConnected || pvView !== name) return; // 拉取期间切走了:过期响应不落盘
|
|
5955
|
+
el.innerHTML = codeBlockHTML(text);
|
|
5956
|
+
} catch (e) {
|
|
5957
|
+
if (el.isConnected && pvView === name)
|
|
5958
|
+
el.innerHTML = '<div class="fp-hint">' + esc(t("内容读取失败:") + (e.message || e)) + "</div>";
|
|
5959
|
+
}
|
|
5960
|
+
}
|
|
5961
|
+
|
|
5962
|
+
function pvDraw() {
|
|
5963
|
+
const box = $("rd-preview");
|
|
5964
|
+
if (!box) return;
|
|
5965
|
+
if (!pvApp || !pvApp.ok) {
|
|
5966
|
+
box.classList.add("hidden");
|
|
5967
|
+
// iframe 一并摘掉:hidden 只是不可见,留在 DOM 里旧页面还在跑(定时器/
|
|
5968
|
+
// 音频不会停);换成无预览的 run 时更不能留上一家的残影
|
|
5969
|
+
const body = $("rd-pv-body");
|
|
5970
|
+
if (body) body.innerHTML = "";
|
|
5971
|
+
return;
|
|
5972
|
+
}
|
|
5973
|
+
box.classList.remove("hidden");
|
|
5974
|
+
$("rd-pv-tabs").innerHTML = pvTabsHTML();
|
|
5975
|
+
const open = $("rd-pv-open");
|
|
5976
|
+
if (open) open.href = urlAuth(pvRunUrl());
|
|
5977
|
+
$("rd-pv-body").innerHTML = pvBodyHTML();
|
|
5978
|
+
const hint = $("rd-pv-hint");
|
|
5979
|
+
if (hint) {
|
|
5980
|
+
// 工作目录路径给出来:预览的是哪个目录一目了然(与「成果」同款可点复制)
|
|
5981
|
+
hint.innerHTML = '<span data-i18n-x>' + esc(t("预览工作目录:")) + "</span>" +
|
|
5982
|
+
'<span class="pv-wd" title="' + esc(t("点击复制")) + '" onclick="copyText(this.textContent)">' +
|
|
5983
|
+
esc(pvApp.workdir || "") + "</span>";
|
|
5984
|
+
hint.classList.remove("hidden");
|
|
5985
|
+
}
|
|
5986
|
+
if (pvView !== "run") { pvCodeSig = pvView; pvLoadCode(pvView); }
|
|
5987
|
+
}
|
|
5988
|
+
|
|
5989
|
+
window.pvGo = function (view) {
|
|
5990
|
+
pvView = view;
|
|
5991
|
+
pvCodeSig = "";
|
|
5992
|
+
pvDraw();
|
|
5993
|
+
};
|
|
5994
|
+
window.pvReload = function () {
|
|
5995
|
+
// 重挂 iframe:加时间戳绕缓存;代码视图则重取当前文件
|
|
5996
|
+
if (pvView === "run") {
|
|
5997
|
+
const f = $("rd-pv-frame");
|
|
5998
|
+
if (f) f.src = urlAuth(pvRunUrl()) + (String(pvRunUrl()).includes("?") ? "&" : "?") + "_=" + Date.now();
|
|
5999
|
+
} else {
|
|
6000
|
+
pvLoadCode(pvView);
|
|
6001
|
+
}
|
|
6002
|
+
};
|
|
6003
|
+
|
|
6004
|
+
async function renderPreview(runId) {
|
|
6005
|
+
const box = $("rd-preview");
|
|
6006
|
+
if (!box) return;
|
|
6007
|
+
if (!runId) { box.classList.add("hidden"); rdTabsSync(); return; }
|
|
6008
|
+
if (pvRunId !== runId) { pvRunId = runId; pvApp = null; pvView = "run"; pvCodeSig = ""; }
|
|
6009
|
+
let d = null;
|
|
6010
|
+
try { d = await api("/api/runs/" + encodeURIComponent(runId) + "/preview"); }
|
|
6011
|
+
catch (e) { d = null; }
|
|
6012
|
+
// 过期闸门:拉取期间切了详情就丢弃响应(同 loadArtifacts 的双入口闸门)
|
|
6013
|
+
if (pvRunId !== runId) return;
|
|
6014
|
+
if (!$("run-detail") || $("run-detail").classList.contains("hidden")) { rdTabsSync(); return; }
|
|
6015
|
+
pvApp = d || { ok: false };
|
|
6016
|
+
const badge = document.querySelector('#rd-tabs .rd-tab[data-tab="preview"] .rd-badge');
|
|
6017
|
+
if (badge) {
|
|
6018
|
+
const n = pvApp && pvApp.ok ? (pvApp.files || []).length : 0;
|
|
6019
|
+
badge.textContent = n ? String(n) : "";
|
|
6020
|
+
badge.className = "rd-badge" + (n ? "" : " hidden");
|
|
6021
|
+
}
|
|
6022
|
+
pvDraw();
|
|
6023
|
+
rdTabsSync(); // 预览可用性(rdTabAvail 认 #rd-preview 的显隐)随之更新
|
|
6024
|
+
}
|
|
6025
|
+
|
|
5893
6026
|
window.copyText = function (t) {
|
|
5894
6027
|
// writeText 返回 promise,无头/非安全上下文会拒绝——必须挂 catch,否则
|
|
5895
6028
|
// 未处理的 promise 拒绝会以控制台错误冒出来(测试断言 0 错误会被它打爆)
|
|
@@ -6173,7 +6306,11 @@ async function toggleLog(runId, rel) {
|
|
|
6173
6306
|
(S.detailTaskKey && S.lastRun && S.lastRun.task_id === S.detailTaskKey) ||
|
|
6174
6307
|
((S.inspData || {}).run || {}).id === runId;
|
|
6175
6308
|
if (!owned) return;
|
|
6176
|
-
|
|
6309
|
+
// 占位符跟状态走:运行中才「等待输出」,终态没产出就是「无输出」——
|
|
6310
|
+
// 否则内置合成器这类不落 CLI 日志的步骤结束后,一直挂「等待输出…」
|
|
6311
|
+
// 假装还在产字(2026-09-22 merge 卡已结束却等待输出案)
|
|
6312
|
+
pre.textContent = r.log ||
|
|
6313
|
+
(r.step_status === "running" ? t("(等待输出…)") : t("(无输出)"));
|
|
6177
6314
|
box.classList.remove("hidden");
|
|
6178
6315
|
syncChatLogSpace(true);
|
|
6179
6316
|
currentLog = { runId, rel };
|
|
@@ -6192,7 +6329,9 @@ async function toggleLog(runId, rel) {
|
|
|
6192
6329
|
if (currentLog && currentLog.runId === runId && currentLog.rel === rel) {
|
|
6193
6330
|
// 贴底跟随:用户滚到底部附近才自动滚到最新输出,回看历史不打扰
|
|
6194
6331
|
const stick = pre.scrollHeight - pre.scrollTop - pre.clientHeight < 48;
|
|
6195
|
-
|
|
6332
|
+
// 最后一帧常在步骤翻终态后才拉到:此刻仍无输出就该换「无输出」
|
|
6333
|
+
pre.textContent = rr.log ||
|
|
6334
|
+
(rr.step_status === "running" ? t("(等待输出…)") : t("(无输出)"));
|
|
6196
6335
|
if (stick) pre.scrollTop = pre.scrollHeight;
|
|
6197
6336
|
logLiveBadge(true); // 每帧都打时间戳:内容没变也能证明通道活着
|
|
6198
6337
|
if (rr.step_status && rr.step_status !== "running") {
|
|
@@ -6678,6 +6817,9 @@ function chatCleanText(raw) {
|
|
|
6678
6817
|
.trim();
|
|
6679
6818
|
}
|
|
6680
6819
|
let chatSig = "";
|
|
6820
|
+
let chatLiveSig = ""; // 时间线实时增量签名(思考/正文长度变才重建 DOM)
|
|
6821
|
+
let chatLiveTimer = null; // 运行中快轮询句柄(1.2s,见 scheduleChatLive)
|
|
6822
|
+
let chatLiveRunId = null;
|
|
6681
6823
|
|
|
6682
6824
|
function chatEngineIsDirect(run) {
|
|
6683
6825
|
const st = S.state || {};
|
|
@@ -6687,6 +6829,54 @@ function chatEngineIsDirect(run) {
|
|
|
6687
6829
|
(!task && run && (run.engine === "direct" || run.type === "direct")));
|
|
6688
6830
|
}
|
|
6689
6831
|
|
|
6832
|
+
/* 思考过程块(2026-09-22 用户诉求「把思考过程打印出来」):内置智能体流式抓的
|
|
6833
|
+
* 模型思维链 + 工具活动。运行中=常开实时块(逐句长出来);结束后=折叠摘要
|
|
6834
|
+
* (点开回看),不再让整轮只有三点打字动画。thinking 最长 2 万字(后端已截)。 */
|
|
6835
|
+
function chatThinkHTML(it) {
|
|
6836
|
+
const running = it.status === "running";
|
|
6837
|
+
const think = String(it.thinking || "").trim();
|
|
6838
|
+
const acts = running ? (it.activity || []).slice(-4) : [];
|
|
6839
|
+
if (!think && !acts.length) return "";
|
|
6840
|
+
const shown = think.length > 12000 ? "…" + think.slice(-12000) : think;
|
|
6841
|
+
if (running) {
|
|
6842
|
+
return '<div class="chat-think live" data-think-live="1">' +
|
|
6843
|
+
'<div class="ct-head"><span class="ct-dot" aria-hidden="true"></span>' +
|
|
6844
|
+
esc(t("思考过程")) + '<span class="ct-live">' + esc(t("实时")) + "</span></div>" +
|
|
6845
|
+
(shown ? '<div class="ct-body">' + esc(shown) + "</div>" : "") +
|
|
6846
|
+
(acts.length ? '<div class="ct-acts">' +
|
|
6847
|
+
acts.map((a) => "<div>" + esc(a) + "</div>").join("") + "</div>" : "") +
|
|
6848
|
+
"</div>";
|
|
6849
|
+
}
|
|
6850
|
+
return '<details class="chat-think"><summary>' + esc(t("思考过程")) +
|
|
6851
|
+
'<span class="ct-len">' + think.length + " " + esc(t("字")) + "</span></summary>" +
|
|
6852
|
+
'<div class="ct-body">' + esc(think) + "</div></details>";
|
|
6853
|
+
}
|
|
6854
|
+
|
|
6855
|
+
/* 运行中直连对话的快轮询:SSE/轮询最密 2s、闲时 8s,思考是逐句吐的——
|
|
6856
|
+
* 1.2s 直拉时间线把增量打印出来。run 终态、切详情、分区隐藏即停。 */
|
|
6857
|
+
function stopChatLive() {
|
|
6858
|
+
if (chatLiveTimer) { clearInterval(chatLiveTimer); chatLiveTimer = null; }
|
|
6859
|
+
chatLiveRunId = null;
|
|
6860
|
+
}
|
|
6861
|
+
|
|
6862
|
+
function scheduleChatLive(runId) {
|
|
6863
|
+
if (chatLiveTimer && chatLiveRunId === runId) return;
|
|
6864
|
+
stopChatLive();
|
|
6865
|
+
chatLiveRunId = runId;
|
|
6866
|
+
chatLiveTimer = setInterval(async () => {
|
|
6867
|
+
const run = S.lastRun;
|
|
6868
|
+
const flow = $("rd-chat-flow");
|
|
6869
|
+
if (!run || run.id !== runId || !flow ||
|
|
6870
|
+
!$("run-detail") || $("run-detail").classList.contains("hidden") ||
|
|
6871
|
+
(run.status !== "running" && run.status !== "queued")) return stopChatLive();
|
|
6872
|
+
try {
|
|
6873
|
+
const data = await api("/api/runs/" + encodeURIComponent(runId) + "/timeline");
|
|
6874
|
+
if (!S.lastRun || S.lastRun.id !== runId) return; // 拉取期间切了详情
|
|
6875
|
+
drawChatFlow(run, data, true);
|
|
6876
|
+
} catch (e) { /* 网络抖动保留上一帧 */ }
|
|
6877
|
+
}, 1200);
|
|
6878
|
+
}
|
|
6879
|
+
|
|
6690
6880
|
/* 对话正文:``` 围栏渲染成真代码块(复用 codeBlockHTML:行号+高亮+代码主题),
|
|
6691
6881
|
* 其余文本保持 pre-wrap 原样(行内 `code` 与 **加粗** 轻量翻译)。模型输出先
|
|
6692
6882
|
* 全量 esc 再插标记;未闭合围栏按代码块收尾,流式输出中途也不撒裸反引号。 */
|
|
@@ -6727,7 +6917,7 @@ async function renderChat(run, active) {
|
|
|
6727
6917
|
box.classList.remove("hidden");
|
|
6728
6918
|
const continueNote = $("rd-chat-continue-note");
|
|
6729
6919
|
if (continueNote) continueNote.classList.toggle("hidden", !!active);
|
|
6730
|
-
if (chatRunId !== run.id) { chatRunId = run.id; chatAtts = []; drawChatAtts(); chatSig = ""; }
|
|
6920
|
+
if (chatRunId !== run.id) { chatRunId = run.id; chatAtts = []; drawChatAtts(); chatSig = ""; chatLiveSig = ""; }
|
|
6731
6921
|
// 三件套回填:任务对象来自 S.state(渲染时已就位);只在任务未在跑时回显,
|
|
6732
6922
|
// 防止轮询把用户正在改的值弹回(select change 重绘弹回教训)
|
|
6733
6923
|
if (run.status !== "running" && run.status !== "queued" && S.state) {
|
|
@@ -6742,12 +6932,29 @@ async function renderChat(run, active) {
|
|
|
6742
6932
|
let data = null;
|
|
6743
6933
|
try { data = await api("/api/runs/" + encodeURIComponent(run.id) + "/timeline"); }
|
|
6744
6934
|
catch (e) { data = null; }
|
|
6745
|
-
const items = (data && data.items) || [];
|
|
6746
6935
|
if (!$("run-detail") || $("run-detail").classList.contains("hidden")) return;
|
|
6747
6936
|
// 过期闸门:run 级(renderRunDetail)与任务级(drawTaskDetail)都经 S.lastRun
|
|
6748
6937
|
// 指向当前详情正在展示的那条 run——拉取期间切了详情就丢弃响应
|
|
6749
6938
|
if (!S.lastRun || S.lastRun.id !== runForFetch) return;
|
|
6939
|
+
drawChatFlow(run, data, active);
|
|
6940
|
+
}
|
|
6941
|
+
|
|
6942
|
+
/* 时间线渲染本体(renderChat 与运行中快轮询共用):items → 气泡流。
|
|
6943
|
+
* force=true 时跳过增量签名闸门(快轮询拿到的一定是新帧)。 */
|
|
6944
|
+
function drawChatFlow(run, data, active) {
|
|
6945
|
+
const items = (data && data.items) || [];
|
|
6750
6946
|
const flow = $("rd-chat-flow");
|
|
6947
|
+
if (!flow) return;
|
|
6948
|
+
// 增量闸门:时间线任何可见输入(状态/正文/思考/实时流/活动/追问/已送达)
|
|
6949
|
+
// 都没变才跳过重建——保滚动位置与折叠态。维度少了不行:只看 live 长度的话,
|
|
6950
|
+
// 没开思考的 CLI 步骤 running→done 两态签名相同,气泡会永远停在打字动画
|
|
6951
|
+
const liveSig = items.reduce((a, it) => a + "|" + (it.kind || "") + ":" +
|
|
6952
|
+
(it.status || "") + ":" + String(it.text || "").length + ":" +
|
|
6953
|
+
String(it.thinking || "").length + ":" + String(it.stream || "").length + ":" +
|
|
6954
|
+
(it.activity || []).length + ":" + (it.followups || []).length +
|
|
6955
|
+
":" + (it.consumed ? 1 : 0), "") + "|" + ((data && data.result) ? "r" : "");
|
|
6956
|
+
if (liveSig === chatLiveSig && flow.childElementCount) return;
|
|
6957
|
+
chatLiveSig = liveSig;
|
|
6751
6958
|
// 时间显示统一收敛到 HH:MM(日期在 meta 条里,全量戳塞气泡就是噪音);
|
|
6752
6959
|
// 附件兼容 字符串路径 / {name|path} 对象 两种形态(对象直接拼会成 [object Object])
|
|
6753
6960
|
const chatTime = (v) => { const s = String(v || "");
|
|
@@ -6798,11 +7005,14 @@ async function renderChat(run, active) {
|
|
|
6798
7005
|
// 开场「本轮做了什么:…」+「回复:」是模型复读协议措辞的元描述(提示词已禁,
|
|
6799
7006
|
// 这里兜历史数据),只剥消息开头的第一行
|
|
6800
7007
|
const body = chatCleanText(it.text);
|
|
6801
|
-
//
|
|
7008
|
+
// 运行中还没有正文:先给思考过程块(有思维链/工具活动时),正文位退化为
|
|
7009
|
+
// 实时预览(stream 累计值);两者皆无才是三点打字动画(终态无正文才落占位)
|
|
7010
|
+
const liveText = it.status === "running" ? String(it.stream || "").trim() : "";
|
|
6802
7011
|
const bodyHtml = body ? chatBodyHTML(body)
|
|
7012
|
+
: (liveText ? '<div class="chat-body chat-live-text">' + esc(liveText.slice(-4000)) + "</div>"
|
|
6803
7013
|
: (it.status === "running"
|
|
6804
7014
|
? '<span class="chat-typing" aria-label="' + esc(t("正在执行…")) + '"><i></i><i></i><i></i></span>'
|
|
6805
|
-
: esc(t("(本轮无文本输出)")));
|
|
7015
|
+
: esc(t("(本轮无文本输出)"))));
|
|
6806
7016
|
// 元信息只留 名字+时间(+失败标记):路由/供应商细节属于「步骤」页签,塞这里就是噪音
|
|
6807
7017
|
const metaBad = it.status === "failed" || it.status === "cancelled";
|
|
6808
7018
|
// 智能体回复:头像 + 整幅正文(不套气泡框),元信息小字落在正文下方;
|
|
@@ -6813,6 +7023,7 @@ async function renderChat(run, active) {
|
|
|
6813
7023
|
return '<div class="chat-row">' +
|
|
6814
7024
|
'<span class="chat-avatar" aria-hidden="true"><svg class="ico"><use href="#i-bee"></use></svg></span>' +
|
|
6815
7025
|
'<div class="chat-bubble agent">' +
|
|
7026
|
+
chatThinkHTML(it) +
|
|
6816
7027
|
'<div class="chat-body">' + bodyHtml + "</div>" +
|
|
6817
7028
|
'<div class="chat-meta">' + esc(it.who || "") + " " + esc(chatTime(it.at)) +
|
|
6818
7029
|
(metaBad ? " · " + t(it.status === "failed" ? "失败" : "已取消") : "") + actsA + "</div>" +
|
|
@@ -6834,8 +7045,14 @@ async function renderChat(run, active) {
|
|
|
6834
7045
|
if (hint) hint.textContent = active
|
|
6835
7046
|
? t("运行中:新消息会排队,本轮回答完后依次送达")
|
|
6836
7047
|
: t("已结束:发送后将自动开新一轮接着做");
|
|
7048
|
+
// 贴底跟随:用户滚到底部附近才自动滚到最新输出,回看历史不打扰
|
|
7049
|
+
const stick = flow.scrollHeight - flow.scrollTop - flow.clientHeight < 80;
|
|
6837
7050
|
flow.innerHTML = html || '<div class="hint">' + esc(t("还没有对话内容")) + "</div>";
|
|
6838
|
-
flow.scrollTop = flow.scrollHeight;
|
|
7051
|
+
if (stick) flow.scrollTop = flow.scrollHeight;
|
|
7052
|
+
// 有运行中的直连步骤 → 快轮询把思考过程/正文增量打印出来;终态即停
|
|
7053
|
+
const hasRunning = items.some((it) => it.kind === "agent" && it.status === "running");
|
|
7054
|
+
if (active && hasRunning && run && run.id) scheduleChatLive(run.id);
|
|
7055
|
+
else stopChatLive();
|
|
6839
7056
|
}
|
|
6840
7057
|
|
|
6841
7058
|
/* 执行结果卡(时间线收尾):run 终态后的确定性摘要——成没成、跑多久、谁执行
|
|
@@ -7932,43 +8149,9 @@ function autoBindAll() {
|
|
|
7932
8149
|
}
|
|
7933
8150
|
}
|
|
7934
8151
|
|
|
7935
|
-
/*
|
|
7936
|
-
*
|
|
7937
|
-
*
|
|
7938
|
-
* 不碰;一处都没改成静默返回,不打扰停用/启用的操作反馈。 */
|
|
7939
|
-
let _autoRebindRunning = false, _autoRebindAgain = false;
|
|
7940
|
-
async function autoRebindSoon() {
|
|
7941
|
-
if (_autoRebindRunning) { _autoRebindAgain = true; return; }
|
|
7942
|
-
_autoRebindRunning = true;
|
|
7943
|
-
try {
|
|
7944
|
-
await poll(); // 拿停用/启用落盘后的最新 providers / bindings / catalog
|
|
7945
|
-
let fixed = 0;
|
|
7946
|
-
for (const c of (S.catalog || []).filter((x) => x.installed && x.orch_kind)) {
|
|
7947
|
-
const st = bindSelById(c.id);
|
|
7948
|
-
if (st.dirty) continue; // 用户手改中,不覆盖草稿
|
|
7949
|
-
const act = bindRepairAction(c, st, false);
|
|
7950
|
-
if (!act) continue;
|
|
7951
|
-
const b = (S.bindings || {})[c.id] || {};
|
|
7952
|
-
try {
|
|
7953
|
-
await api("/api/models/binding", { method: "POST", busy: false, body: JSON.stringify({
|
|
7954
|
-
agent_id: c.id, provider_id: act[0].p,
|
|
7955
|
-
chain: act.map((x) => ({ provider_id: x.p, model: x.m })),
|
|
7956
|
-
difficulty_routing: !!b.difficulty_routing }) });
|
|
7957
|
-
st.chain = act; st.dirty = false; st.key = chainKey(act);
|
|
7958
|
-
fixed++;
|
|
7959
|
-
} catch (e) { /* 单条失败不打断,等下次变更再补 */ }
|
|
7960
|
-
}
|
|
7961
|
-
if (fixed) {
|
|
7962
|
-
S.catSig = null; S.bindSig = null;
|
|
7963
|
-
render();
|
|
7964
|
-
toast(t("厂商/模型变动,已自动重绑 %1 处。").replace("%1", fixed));
|
|
7965
|
-
}
|
|
7966
|
-
} catch (e) { /* 自动补绑失败静默:不打断用户的停用/启用操作 */ }
|
|
7967
|
-
finally {
|
|
7968
|
-
_autoRebindRunning = false;
|
|
7969
|
-
if (_autoRebindAgain) { _autoRebindAgain = false; autoRebindSoon(); }
|
|
7970
|
-
}
|
|
7971
|
-
}
|
|
8152
|
+
/* 厂商/模型停用·删除后不再自动改写绑定链(2026-09-22 用户拍板:不要去动
|
|
8153
|
+
* 模型调度)——后端在停用/删除时已把死条目从链里剔除(modelhub._prune_binding_chains),
|
|
8154
|
+
* 空链回落 CLI 默认;要补推荐用「一键推荐绑定」手动来。 */
|
|
7972
8155
|
|
|
7973
8156
|
async function saveAllBindings() {
|
|
7974
8157
|
const targets = (S.catalog || []).filter((c) => c.installed && c.orch_kind);
|
|
@@ -9142,7 +9325,7 @@ async function loadZentao() {
|
|
|
9142
9325
|
tg("zt-autoresolve", cfg.auto_resolve);
|
|
9143
9326
|
tg("zt-automerge", cfg.auto_merge);
|
|
9144
9327
|
tg("zt-poll", cfg.poll_enabled);
|
|
9145
|
-
set("zt-interval", cfg.
|
|
9328
|
+
set("zt-interval", cfg.interval_minutes || 5);
|
|
9146
9329
|
S.ztProfiles = JSON.parse(JSON.stringify(cfg.product_profiles || []));
|
|
9147
9330
|
renderZentaoProfiles();
|
|
9148
9331
|
renderZentaoStatus();
|
|
@@ -9513,6 +9696,13 @@ function zentaoTriText(c) {
|
|
|
9513
9696
|
return t("排查:") + t(NAME[tri.side] || tri.side) + (by ? "(" + by + ")" : "");
|
|
9514
9697
|
}
|
|
9515
9698
|
|
|
9699
|
+
/* 扫描间隔(分钟)→ 人类可读:整小时显示「2 小时」,否则「5 分钟」 */
|
|
9700
|
+
function ztIntervalText(mins) {
|
|
9701
|
+
const n = Math.max(1, parseInt(mins, 10) || 5);
|
|
9702
|
+
if (n % 60 === 0) return (n / 60) + t(" 小时一次");
|
|
9703
|
+
return n + t(" 分钟一次");
|
|
9704
|
+
}
|
|
9705
|
+
|
|
9516
9706
|
function renderZentaoStatus() {
|
|
9517
9707
|
const box = $("zentao-status");
|
|
9518
9708
|
if (!box) return;
|
|
@@ -9521,7 +9711,7 @@ function renderZentaoStatus() {
|
|
|
9521
9711
|
const cfg = v.config || {};
|
|
9522
9712
|
const parts = [];
|
|
9523
9713
|
parts.push(cfg.poll_enabled
|
|
9524
|
-
? (t("定时扫描已开启,每 ") + (cfg.
|
|
9714
|
+
? (t("定时扫描已开启,每 ") + ztIntervalText(cfg.interval_minutes)
|
|
9525
9715
|
+ (v.next_scan ? t(",下次 ") + esc(v.next_scan) : ""))
|
|
9526
9716
|
: t("定时扫描未开启(仍可手动「立即扫描」)"));
|
|
9527
9717
|
if (v.last_scan) parts.push(t("上次扫描 ") + esc(v.last_scan));
|
|
@@ -9565,7 +9755,7 @@ function ztFormPayload() {
|
|
|
9565
9755
|
auto_resolve: $("zt-autoresolve").checked,
|
|
9566
9756
|
auto_merge: $("zt-automerge").checked,
|
|
9567
9757
|
poll_enabled: $("zt-poll").checked,
|
|
9568
|
-
|
|
9758
|
+
interval_minutes: Math.max(5, parseInt($("zt-interval").value, 10) || 5),
|
|
9569
9759
|
};
|
|
9570
9760
|
const pw = ($("zt-password").value || "").trim();
|
|
9571
9761
|
if (pw) payload.password = pw;
|
|
@@ -9985,6 +10175,8 @@ async function loadSettings() {
|
|
|
9985
10175
|
if (tel && S.settings) tel.checked = S.settings.telemetry_errors !== false;
|
|
9986
10176
|
const pet = $("set-pet");
|
|
9987
10177
|
if (pet && S.settings) pet.checked = S.settings.pet_enabled !== false;
|
|
10178
|
+
const cs = $("set-claude-sync");
|
|
10179
|
+
if (cs && S.settings) cs.checked = S.settings.claude_config_sync !== false;
|
|
9988
10180
|
syncPetModeSeg();
|
|
9989
10181
|
syncPetSkinSeg();
|
|
9990
10182
|
const wd = $("set-workdir"), hint = $("set-workdir-hint");
|
|
@@ -10169,7 +10361,8 @@ async function saveSettings() {
|
|
|
10169
10361
|
const msg = $("settings-msg");
|
|
10170
10362
|
try {
|
|
10171
10363
|
const r = await api("/api/settings", { method: "POST", body: JSON.stringify({
|
|
10172
|
-
max_concurrent_jobs: parseInt($("set-workers").value, 10)
|
|
10364
|
+
max_concurrent_jobs: parseInt($("set-workers").value, 10),
|
|
10365
|
+
claude_config_sync: !$("set-claude-sync") || $("set-claude-sync").checked }) });
|
|
10173
10366
|
S.settings = r.settings;
|
|
10174
10367
|
if (msg) { msg.className = "msg ok"; msg.textContent = t("已保存:最大并发 ") + r.workers + t(" 个任务"); }
|
|
10175
10368
|
} catch (e) {
|
|
@@ -10278,6 +10471,9 @@ function helpChapterBody(id) {
|
|
|
10278
10471
|
'<p class="help-p">' + t("新建任务选「连载写作」类型,目标里写清书名、题材和计划篇幅(如「都市异能,先写 20 章」)。目标写得太笼统时,蜂群会先反问几个澄清问题再开工。开书前会先准备作品资料:书简介与分类标签可一键生成,按平台官方选项实抓,不用自己去查规则。") + '</p>' +
|
|
10279
10472
|
'<h3 class="help-h3">' + t("每章怎么跑") + '</h3>' +
|
|
10280
10473
|
'<p class="help-p">' + t("大纲确认后分章推进:起草 → 跨厂商评审 → 打磨 → 定稿。评审不过会自动换将重写,不带着问题过关;需要你拍板的地方会亮起「待裁决」。写作时详情页可实时预览章节,蜂巢页每个智能体一格,点开能看谁在干什么。") + '</p>' +
|
|
10474
|
+
'<h3 class="help-h3">' + t("分卷(卷结构)") + '</h3>' +
|
|
10475
|
+
'<p class="help-p">' + t("长篇建议分卷。两种给法:填「每卷章数」(默认预填 20,清空 = 不分卷),系统按全书章号自动切卷;或者直接写明各卷——在「指定卷结构」框里,也可以在任务目标里写「第一卷 少年初入江湖 第1-20章;卷二 风云再起 21-40章」,会自动识别。给了卷名的会原样沿用,不会被改写。") + '</p>' +
|
|
10476
|
+
'<p class="help-p">' + t("分卷会贯穿全程:大纲按卷给卷名与卷弧光(本卷主线冲突与卷末高潮);每章起草和评审都带着「第几卷第几章」,靠近卷末会提醒收拢支线、卷末章必须写出本卷高潮与卷末钩子;成书自动插入「第 X 卷《卷名》」分隔;详情页章节卡与发布页清单按卷分组,发章时一卷一卷地发。卷边界只看全书章号,所以续写批次会自动接上同一卷,不会把一卷劈成两半。") + '</p>' +
|
|
10281
10477
|
'<h3 class="help-h3">' + t("中断了怎么办") + '</h3>' +
|
|
10282
10478
|
'<p class="help-p">' + t("任务随时可续跑:已写章节都落了盘,续跑接着往下写,不会从头再来。单章失败会自动退避重试,任务状态里会显示「将于 HH:MM 自动续跑」,不用盯着等。") + '</p>' +
|
|
10283
10479
|
'<h3 class="help-h3">' + t("递话与经验") + '</h3>' +
|
|
@@ -10715,6 +10911,29 @@ function tabTitle(name) {
|
|
|
10715
10911
|
return t(TAB_TITLES[name]) || t("设置");
|
|
10716
10912
|
}
|
|
10717
10913
|
|
|
10914
|
+
/* 顶栏标题 + 面包屑:页名永远落在 #page-title(多处测试断言它的 textContent
|
|
10915
|
+
* 精确等于页名,所以组名绝不能并进去),父级组名单独放 #crumb-root。
|
|
10916
|
+
* 组名从左栏设置导航现读——找选中项上方最近的分组标签,HTML 增删导航项时
|
|
10917
|
+
* 这里自动跟上,不需要维护一份硬编码映射。全页统一切标题只走这一个函数。 */
|
|
10918
|
+
function renderPageCrumb() {
|
|
10919
|
+
const el = $("page-title");
|
|
10920
|
+
if (!el) return;
|
|
10921
|
+
const crumb = $("crumb-root");
|
|
10922
|
+
const inDetail = !!(S.detailRunId || S.detailTaskKey); // 详情自成语境,不挂父级
|
|
10923
|
+
let group = "";
|
|
10924
|
+
if (!inDetail && document.body.classList.contains("settings-mode")) {
|
|
10925
|
+
const active = document.querySelector(".side-settings .set-item.active");
|
|
10926
|
+
let n = active ? active.previousElementSibling : null;
|
|
10927
|
+
while (n && !(n.classList && n.classList.contains("set-group"))) n = n.previousElementSibling;
|
|
10928
|
+
if (n) group = (n.textContent || "").trim();
|
|
10929
|
+
}
|
|
10930
|
+
if (crumb) {
|
|
10931
|
+
crumb.textContent = group;
|
|
10932
|
+
crumb.classList.toggle("hidden", !group);
|
|
10933
|
+
}
|
|
10934
|
+
el.textContent = inDetail ? t("运行详情") : tabTitle(S.tab);
|
|
10935
|
+
}
|
|
10936
|
+
|
|
10718
10937
|
/* 左栏两套导航各管哪些页,一律从 index.html 现读——HTML 改了 JS 自动跟上,
|
|
10719
10938
|
* 不再各存一份常量、两处漂移:
|
|
10720
10939
|
* .side-quick 快捷导航(概览/运行记录/自动化)→ main 形态:只换主栏内容,左栏任务树不动
|
|
@@ -10895,9 +11114,8 @@ function setLangBtn(lang) {
|
|
|
10895
11114
|
paintHelpChrome();
|
|
10896
11115
|
renderHelp();
|
|
10897
11116
|
}
|
|
10898
|
-
//
|
|
10899
|
-
|
|
10900
|
-
if (title && S.tab) title.textContent = tabTitle(S.tab);
|
|
11117
|
+
// 重画还在缓存里的页面标题(语言切换后组名也要跟着换)
|
|
11118
|
+
renderPageCrumb();
|
|
10901
11119
|
// 同步顶栏的连接状态文案(applyI18n 不会处理 textContent 写入的)
|
|
10902
11120
|
const cn = $("conn");
|
|
10903
11121
|
if (cn) {
|
|
@@ -11556,11 +11774,79 @@ function ovRenderRecent() {
|
|
|
11556
11774
|
}
|
|
11557
11775
|
|
|
11558
11776
|
/* 运行状态经 SSE 推来:停在概览页时只重画依赖 runs 的两处(概览语 + 最近运行),
|
|
11559
|
-
*
|
|
11777
|
+
* 不重新拉用量(台账不会因为一次运行状态变化就变)。
|
|
11778
|
+
* 侧栏常驻件(用量条 + 计数徽章)不跟着 S.tab 走,每次状态推送都要顺手刷新。 */
|
|
11560
11779
|
function ovMaybeRefresh() {
|
|
11561
|
-
if (S.tab
|
|
11562
|
-
|
|
11563
|
-
|
|
11780
|
+
if (S.tab === "overview") {
|
|
11781
|
+
ovRenderHead();
|
|
11782
|
+
ovRenderRecent();
|
|
11783
|
+
}
|
|
11784
|
+
sideUsageRefresh();
|
|
11785
|
+
}
|
|
11786
|
+
|
|
11787
|
+
/* ---------------------------------------------------------- 侧栏常驻:用量条 + 计数徽章 */
|
|
11788
|
+
/* 用量条的数字来自真实台账,口径写死为「本月」(自然月,从 1 号起)。
|
|
11789
|
+
* 刻意不做「用量 68%」那种进度条:CodeBee 没有套餐配额,没有分母可除,
|
|
11790
|
+
* 编一个上限出来就是假数。这里陈列的是能直接去用量页核对的事实。 */
|
|
11791
|
+
async function sideUsageLoad() {
|
|
11792
|
+
if (S.sideUsage && S.sideUsageAt && Date.now() - S.sideUsageAt < 60000) return; // 1 分钟节流
|
|
11793
|
+
try {
|
|
11794
|
+
const u = await api("/api/usage?days=30");
|
|
11795
|
+
S.sideUsage = u || null;
|
|
11796
|
+
S.sideUsageAt = Date.now();
|
|
11797
|
+
} catch (e) { /* 拉不到就保留上次的值,不把侧栏清空 */ }
|
|
11798
|
+
sideUsageRender();
|
|
11799
|
+
}
|
|
11800
|
+
|
|
11801
|
+
function sideUsageRefresh() {
|
|
11802
|
+
sideUsageRender(); // 计数徽章靠 S.state,同步重画
|
|
11803
|
+
sideUsageLoad(); // 台账按节流异步补
|
|
11804
|
+
}
|
|
11805
|
+
|
|
11806
|
+
function sideUsageRender() {
|
|
11807
|
+
sideUsageRenderRuns();
|
|
11808
|
+
const box = $("side-usage");
|
|
11809
|
+
if (!box) return;
|
|
11810
|
+
const u = S.sideUsage;
|
|
11811
|
+
const tok = $("su-tok"), cost = $("su-cost"), spark = $("su-spark");
|
|
11812
|
+
if (!tok || !cost || !spark) return;
|
|
11813
|
+
if (!u || !u.by_day || !u.by_day.length) {
|
|
11814
|
+
tok.textContent = t("本月还没有调用");
|
|
11815
|
+
cost.textContent = "";
|
|
11816
|
+
spark.innerHTML = "";
|
|
11817
|
+
return;
|
|
11818
|
+
}
|
|
11819
|
+
// 口径=自然月:days=31 只是取数窗口(任一自然月的已过天数都不超过它),
|
|
11820
|
+
// 展示前必须按当前年月再滤一遍——不滤的话月初那几天会把上个月的尾巴算进"本月",
|
|
11821
|
+
// 标签写着本月、数字却是滚动 30 天,就是假口径
|
|
11822
|
+
const now = new Date();
|
|
11823
|
+
const ym = now.getFullYear() + "-" + String(now.getMonth() + 1).padStart(2, "0");
|
|
11824
|
+
const monthDays = u.by_day.filter((d) => String(d.day || "").slice(0, 7) === ym);
|
|
11825
|
+
const sum = (k) => monthDays.reduce((s, d) => s + (Number(d[k]) || 0), 0);
|
|
11826
|
+
const calls = sum("calls");
|
|
11827
|
+
if (!calls) {
|
|
11828
|
+
tok.textContent = t("本月还没有调用");
|
|
11829
|
+
cost.textContent = "";
|
|
11830
|
+
spark.innerHTML = "";
|
|
11831
|
+
return;
|
|
11832
|
+
}
|
|
11833
|
+
tok.textContent = fmtTok(sum("tokens")) + t(" tokens · ") + fmtTok(calls) + t(" 次");
|
|
11834
|
+
cost.textContent = fmtUsd(sum("cost_usd"));
|
|
11835
|
+
// 走势不受月界影响:近 14 天日序列,只是节奏参考,by_day 连续补零直接取尾
|
|
11836
|
+
const days = u.by_day.slice(-14).map((d) => d.tokens || 0);
|
|
11837
|
+
spark.innerHTML = days.length > 1 ? kpiSparkSvg(days, "accent") : "";
|
|
11838
|
+
}
|
|
11839
|
+
|
|
11840
|
+
/* 计数徽章:挂在「运行记录」快捷导航项上,数当前在跑 + 排队的 run 数。
|
|
11841
|
+
* 数据取自 S.state.runs(SSE 推送),不额外请求;为 0 时收起徽章不留空位。 */
|
|
11842
|
+
function sideUsageRenderRuns() {
|
|
11843
|
+
const el = $("nav-badge-runs");
|
|
11844
|
+
if (!el) return;
|
|
11845
|
+
const runs = (S.state && S.state.runs) || [];
|
|
11846
|
+
const n = runs.filter((r) => r.status === "running" || r.status === "queued").length;
|
|
11847
|
+
el.textContent = n > 99 ? "99+" : String(n);
|
|
11848
|
+
el.classList.toggle("hidden", n === 0);
|
|
11849
|
+
el.title = n ? t("正在运行或排队 ") + n + t(" 个任务") : "";
|
|
11564
11850
|
}
|
|
11565
11851
|
|
|
11566
11852
|
/* ---------------------------------------------------------- 手机连接(扫码) */
|
|
@@ -11838,8 +12124,7 @@ function switchTab(name, shell) {
|
|
|
11838
12124
|
document.querySelectorAll(".set-item").forEach((b) => b.classList.toggle("active", !inMain && b.dataset.sub === name));
|
|
11839
12125
|
document.querySelectorAll(".qitem").forEach((b) => b.classList.toggle("active", inMain && b.dataset.page === name));
|
|
11840
12126
|
document.querySelectorAll("#page-settings .subpage").forEach((d) => d.classList.toggle("hidden", d.id !== "sub-" + name));
|
|
11841
|
-
|
|
11842
|
-
if (title) title.textContent = tabTitle(name);
|
|
12127
|
+
renderPageCrumb(); // 页名 + 面包屑组名一起刷新(组名从左栏导航现读)
|
|
11843
12128
|
if (name === "runs" && !S.detailRunId) closeRun();
|
|
11844
12129
|
if (name === "agents") autoCheckUpdates(); // 进目录页自动查各 CLI 新版本
|
|
11845
12130
|
if (name === "orch") { loadOrchestrator(); loadSettings(); } // 进编排设置页拉取配置
|
|
@@ -11873,8 +12158,7 @@ function exitSettings() {
|
|
|
11873
12158
|
document.body.classList.remove("settings-mode");
|
|
11874
12159
|
document.body.classList.remove("files-mode");
|
|
11875
12160
|
cmpGreeting(); // 回到任务页:问候语刷新
|
|
11876
|
-
|
|
11877
|
-
if (title) title.textContent = tabTitle("tasks");
|
|
12161
|
+
renderPageCrumb();
|
|
11878
12162
|
collapseDrawerIfMobile();
|
|
11879
12163
|
syncInspectorVis(); // 回到新建表单:不是任务上下文,检查器收起(点运行详情才会滑回)
|
|
11880
12164
|
}
|
|
@@ -11912,6 +12196,7 @@ window.mgmt = mgmt;
|
|
|
11912
12196
|
window.switchTab = switchTab;
|
|
11913
12197
|
window.setUsageDays = setUsageDays;
|
|
11914
12198
|
window.setOvDays = setOvDays;
|
|
12199
|
+
window.renderPageCrumb = renderPageCrumb; // i18n.js 换语言后补画标题用
|
|
11915
12200
|
window.jumpToRun = jumpToRun;
|
|
11916
12201
|
window.saveProvider = saveProvider;
|
|
11917
12202
|
window.delProvider = delProvider;
|
|
@@ -12026,6 +12311,14 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
12026
12311
|
};
|
|
12027
12312
|
bindArtifactClicks($("rd-arts"));
|
|
12028
12313
|
bindArtifactClicks($("insp-artifacts"));
|
|
12314
|
+
// 预览子页签(运行 / 各文件):委托一次,页签条整块重画也不用重绑
|
|
12315
|
+
const pvTabs = $("rd-pv-tabs");
|
|
12316
|
+
if (pvTabs) pvTabs.addEventListener("click", (e) => {
|
|
12317
|
+
const b = e.target.closest(".pv-tab");
|
|
12318
|
+
if (b && b.dataset.pv) window.pvGo(b.dataset.pv);
|
|
12319
|
+
});
|
|
12320
|
+
const pvReload = $("rd-pv-reload");
|
|
12321
|
+
if (pvReload) pvReload.addEventListener("click", () => window.pvReload());
|
|
12029
12322
|
// 动态详情区统一用 data 属性委托,日志路径/文件名不再拼进 inline JS。
|
|
12030
12323
|
const detailOverview = $("rd-overview");
|
|
12031
12324
|
if (detailOverview) detailOverview.addEventListener("click", (e) => {
|
|
@@ -12179,6 +12472,8 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
12179
12472
|
$("btn-notify-toggle").addEventListener("click", toggleNotifySound);
|
|
12180
12473
|
// 命令面板:侧栏搜索行 / Ctrl+K 唤起(函数若尚未落地,跳过而不炸整个初始化)
|
|
12181
12474
|
if (typeof bindCmdK === "function") bindCmdK();
|
|
12475
|
+
// 侧栏常驻用量条:首屏先取一次台账(之后由 applyState 的节流刷新接手)
|
|
12476
|
+
sideUsageRefresh();
|
|
12182
12477
|
// 文件夹全部展开/收起(写回 localStorage,与手点单个 folder 同一套持久化)
|
|
12183
12478
|
$("btn-side-expand").addEventListener("click", () => {
|
|
12184
12479
|
const dlist = Array.from($("side-tasks").querySelectorAll("details.sdir"));
|