codebee 0.1.5 → 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 +16 -0
- package/README.md +431 -422
- package/app/core/automation.py +30 -5
- package/app/core/catalog.py +23 -0
- package/app/core/health.py +12 -1
- package/app/core/jobs.py +10 -2
- package/app/core/manager.py +87 -10
- package/app/core/modelhub.py +2883 -2845
- package/app/core/pipeline.py +120 -60
- package/app/core/registry.py +4 -0
- package/app/core/remote.py +310 -303
- package/app/core/router.py +5 -4
- package/app/core/runner.py +38 -6
- package/app/core/selfupdate.py +53 -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 +687 -222
- package/app/ui/i18n.js +43 -6
- package/app/ui/icons/bee.svg +79 -0
- package/app/ui/index.html +59 -46
- package/app/ui/style.css +1229 -328
- 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");
|
|
@@ -567,7 +600,9 @@ function renderHealthBanner(health) {
|
|
|
567
600
|
el.className = "health-banner alerting";
|
|
568
601
|
el.title = alerts.map((p) =>
|
|
569
602
|
p.provider + (p.model ? " · " + p.model : "") +
|
|
570
|
-
|
|
603
|
+
(p.static
|
|
604
|
+
? t(":") + (p.last_error || t("未知错误"))
|
|
605
|
+
: t(":连续失败 ") + p.consecutive_failures + t(" 次(") + (p.last_error || t("未知错误")) + t(")"))
|
|
571
606
|
).join("\n");
|
|
572
607
|
if (!_healthBeeped) { _healthBeeped = true; beepAttention(); }
|
|
573
608
|
}
|
|
@@ -575,7 +610,7 @@ function renderHealthBanner(health) {
|
|
|
575
610
|
function healthBannerText(health) {
|
|
576
611
|
return ((health && health.alerts) || []).map((p) => {
|
|
577
612
|
const m = p.model ? " · " + p.model : "";
|
|
578
|
-
return "⚠ " + p.provider + m + " " + t("连接异常");
|
|
613
|
+
return "⚠ " + p.provider + m + " " + (p.static ? t("绑定链失效") : t("连接异常"));
|
|
579
614
|
}).join(t(" "));
|
|
580
615
|
}
|
|
581
616
|
|
|
@@ -584,19 +619,21 @@ function healthBannerClick() {
|
|
|
584
619
|
if (!alerts.length) return;
|
|
585
620
|
const rows = alerts.map((p) => {
|
|
586
621
|
const model = p.model ? " · " + p.model : "";
|
|
587
|
-
|
|
588
|
-
"<b>" + esc(p.provider) + (p.model ? " · " + esc(p.model) : "") + "</b>" +
|
|
622
|
+
const statLine = p.static ? "" :
|
|
589
623
|
"<div style='opacity:.75;font-size:12px;margin-top:2px'>" +
|
|
590
624
|
t("连续失败") + " " + p.consecutive_failures + " " + t("次 · 首次失败 ") + (p.first_fail_at || "-") +
|
|
591
|
-
"</div>"
|
|
625
|
+
"</div>";
|
|
626
|
+
return "<div style='margin-bottom:10px'>" +
|
|
627
|
+
"<b>" + esc(p.provider) + (p.model ? " · " + esc(p.model) : "") + "</b>" +
|
|
628
|
+
statLine +
|
|
592
629
|
"<div style='color:#dc2626;font-size:12px;margin-top:2px;word-break:break-all'>" +
|
|
593
630
|
esc(p.last_error || "") + "</div></div>";
|
|
594
631
|
}).join("");
|
|
595
632
|
const pid0 = alerts[0].provider_id || "";
|
|
596
633
|
const model0 = alerts[0].model || "";
|
|
597
634
|
const foot =
|
|
598
|
-
(model0 ? "<button class='btn ghost' onclick=\"healthDisableModel('" + esc(pid0) + "','" + esc(model0) + "')\">" + t("禁用该模型") + "</button>" : "") +
|
|
599
|
-
"<button class='btn ghost' onclick=\"healthDisableProvider('" + esc(pid0) + "')\">" + t("禁用该厂商") + "</button>" +
|
|
635
|
+
(pid0 && model0 ? "<button class='btn ghost' onclick=\"healthDisableModel('" + esc(pid0) + "','" + esc(model0) + "')\">" + t("禁用该模型") + "</button>" : "") +
|
|
636
|
+
(pid0 ? "<button class='btn ghost' onclick=\"healthDisableProvider('" + esc(pid0) + "')\">" + t("禁用该厂商") + "</button>" : "") +
|
|
600
637
|
"<button class='btn ghost' onclick=\"healthOp('silence')\">" + t("静默本次告警") + "</button>" +
|
|
601
638
|
"<button class='btn ghost' onclick=\"healthOp('reset')\">" + t("手动标记恢复") + "</button>" +
|
|
602
639
|
"<button class='primary' onclick='closeModal()'>" + t("关闭") + "</button>";
|
|
@@ -640,6 +677,7 @@ async function healthDisableModel(pid, model) {
|
|
|
640
677
|
S.provSig = "";
|
|
641
678
|
closeModal(); render(); loadOrchestrator();
|
|
642
679
|
toast(t("已禁用模型 {0} · {1},链降级自动跳过;绑定页可重新启用").replace("{0}", pid).replace("{1}", model));
|
|
680
|
+
autoRebindSoon();
|
|
643
681
|
} catch (e) {
|
|
644
682
|
toast(t("操作失败:") + e.message, true);
|
|
645
683
|
}
|
|
@@ -663,6 +701,7 @@ async function healthDisableProvider(pid) {
|
|
|
663
701
|
S.provSig = "";
|
|
664
702
|
closeModal(); render(); loadOrchestrator();
|
|
665
703
|
toast(t("已禁用厂商 {0}:链降级自动跳过,绑定页可重新启用").replace("{0}", pid));
|
|
704
|
+
autoRebindSoon();
|
|
666
705
|
} catch (e) {
|
|
667
706
|
toast(t("操作失败:") + e.message, true);
|
|
668
707
|
}
|
|
@@ -848,19 +887,19 @@ function renderBindings() {
|
|
|
848
887
|
}).join("");
|
|
849
888
|
const bound = provs.find((p) => p.id === b.provider_id);
|
|
850
889
|
const offWarn = bound && bound.enabled === false
|
|
851
|
-
? '<p class="hint warn">' + t("
|
|
890
|
+
? '<p class="hint warn">' + t("该供应商已停用:编排时不会注入它,模型链因此失效时相关步骤会直接判失败。") + '</p>' : "";
|
|
852
891
|
const protoWarn = bound && !bindable.some((p) => p.id === bound.id)
|
|
853
892
|
? '<p class="hint warn">' + t("该供应商协议为 ") + esc(protoLabel(bound)) +
|
|
854
|
-
t(",当前没有可注入的 CLI
|
|
855
|
-
//
|
|
856
|
-
//
|
|
857
|
-
//
|
|
893
|
+
t(",当前没有可注入的 CLI,编排时相关步骤会直接判失败。") + '</p>' : "";
|
|
894
|
+
// 供应商停用但链上勾选了它家模型:链条目在解析时也会被整条跳过,链空了
|
|
895
|
+
// 相关步骤会直接判失败(2026-09-17 起;此前的静默回落本机默认正是 2026-09-16
|
|
896
|
+
// 配额事故的隐藏形态)——单说「供应商下拉」警告不够,链本身的死活也要点名。
|
|
858
897
|
const chainDead = bindChain(b).some((c2) => c2.p && (() => {
|
|
859
898
|
const pv = provs.find((p) => p.id === c2.p);
|
|
860
899
|
return !pv || pv.enabled === false;
|
|
861
900
|
})());
|
|
862
901
|
const chainWarn = (!bound || bound.enabled !== false) && chainDead
|
|
863
|
-
? '<p class="hint warn">' + t("
|
|
902
|
+
? '<p class="hint warn">' + t("模型链里有已停用/已删除的供应商:这些条目解析时会被跳过,链可能因此整体失效,相关步骤将判失败。") + '</p>' : "";
|
|
864
903
|
return '<div class="card"><div class="head"><span class="name">' + esc(c.name) + "</span>" +
|
|
865
904
|
'<span class="tag">' + esc(c.orch_kind) + "</span></div>" +
|
|
866
905
|
'<div class="field"><label>' + t("供应商") + '</label><select id="bindprov-' + esc(c.id) + '">' + opts + "</select></div>" +
|
|
@@ -964,6 +1003,7 @@ async function batchProvOp(op) {
|
|
|
964
1003
|
try {
|
|
965
1004
|
await api("/api/models/provider-op", { method: "POST",
|
|
966
1005
|
body: JSON.stringify({ ids, op }) });
|
|
1006
|
+
if (op !== "duplicate") autoRebindSoon();
|
|
967
1007
|
} catch (e) { toast(t("操作失败:") + e.message, true); }
|
|
968
1008
|
S.selProvs = {};
|
|
969
1009
|
S.modelsSig = null;
|
|
@@ -1150,6 +1190,7 @@ async function keyOpCall(pid, op, keyId, key, label) {
|
|
|
1150
1190
|
try {
|
|
1151
1191
|
await api("/api/models/key-op", { method: "POST",
|
|
1152
1192
|
body: JSON.stringify({ provider_id: pid, op, key_id: keyId, key, label }) });
|
|
1193
|
+
autoRebindSoon(); // 密钥启停/删除/解冻会改变厂商「推荐可用」判定,自动补绑一次
|
|
1153
1194
|
} catch (e) { toast(t("操作失败:") + e.message, true); }
|
|
1154
1195
|
// 操作期间轮询照跑(uiPrompt 弹框不阻塞轮询),S.modelsSig 还是旧值:必须
|
|
1155
1196
|
// 清签名强制重绘,否则 KEY 写盘成功界面也看不到。
|
|
@@ -1359,6 +1400,7 @@ async function modelOp(pid, name, op) {
|
|
|
1359
1400
|
try {
|
|
1360
1401
|
await api("/api/models/model-op", { method: "POST",
|
|
1361
1402
|
body: JSON.stringify({ provider_id: pid, name, op }) });
|
|
1403
|
+
autoRebindSoon(); // 模型停用/启用/删除会改变推荐源与死链判定,自动补绑一次
|
|
1362
1404
|
const s = modelSel(pid); // 单行操作后同步清掉该行勾选
|
|
1363
1405
|
if (s[name]) { delete modelSelSet(pid)[name]; }
|
|
1364
1406
|
} catch (e) { toast(t("操作失败:") + e.message, true); }
|
|
@@ -1425,6 +1467,7 @@ async function batchModelOp(pid, op) {
|
|
|
1425
1467
|
try {
|
|
1426
1468
|
await api("/api/models/model-op", { method: "POST",
|
|
1427
1469
|
body: JSON.stringify({ provider_id: pid, names, op }) });
|
|
1470
|
+
autoRebindSoon();
|
|
1428
1471
|
} catch (e) { toast(t("操作失败:") + e.message, true); }
|
|
1429
1472
|
clearModelSel(pid);
|
|
1430
1473
|
poll();
|
|
@@ -1443,6 +1486,7 @@ async function toggleProviderEnabled(pid, enabled) {
|
|
|
1443
1486
|
try {
|
|
1444
1487
|
await api("/api/models/provider-op", { method: "POST",
|
|
1445
1488
|
body: JSON.stringify({ ids: [pid], op: off ? "enable" : "disable" }) });
|
|
1489
|
+
autoRebindSoon();
|
|
1446
1490
|
} catch (e) { toast(t("操作失败:") + e.message, true); }
|
|
1447
1491
|
S.modelsSig = null;
|
|
1448
1492
|
poll();
|
|
@@ -1466,7 +1510,8 @@ async function refreshAllModels() {
|
|
|
1466
1510
|
btn.disabled = false; btn.classList.remove("loading");
|
|
1467
1511
|
btn.title = t("全部重新拉取模型列表");
|
|
1468
1512
|
}, 1500);
|
|
1469
|
-
setTimeout(poll, 2500);
|
|
1513
|
+
setTimeout(poll, 2500);
|
|
1514
|
+
setTimeout(autoRebindSoon, 8000); // 模型列表落盘后推荐源才完整,补绑一次
|
|
1470
1515
|
}
|
|
1471
1516
|
|
|
1472
1517
|
async function refreshProviderModels(id) {
|
|
@@ -1477,7 +1522,7 @@ async function refreshProviderModels(id) {
|
|
|
1477
1522
|
else toast(t("已获取模型列表 · wire 适配测试后台进行中"));
|
|
1478
1523
|
} catch (e) { toast(t("获取失败:") + e.message, true); }
|
|
1479
1524
|
poll();
|
|
1480
|
-
setTimeout(
|
|
1525
|
+
setTimeout(autoRebindSoon, 4000); // 模型列表落盘后补绑一次(后台 wire 探测同步进行)
|
|
1481
1526
|
}
|
|
1482
1527
|
|
|
1483
1528
|
/* 供应商编辑卡:协议可选「自动」;显式选定时它是主协议(用于需要唯一协议的
|
|
@@ -1533,6 +1578,7 @@ async function delProvider(id) {
|
|
|
1533
1578
|
try {
|
|
1534
1579
|
await api("/api/models/provider-op", { method: "POST",
|
|
1535
1580
|
body: JSON.stringify({ ids: [id], op: "delete" }) });
|
|
1581
|
+
autoRebindSoon();
|
|
1536
1582
|
} catch (e) { toast(t("操作失败:") + e.message, true); }
|
|
1537
1583
|
if (S.selModels) delete S.selModels[id];
|
|
1538
1584
|
if (S.selProvs) delete S.selProvs[id];
|
|
@@ -1596,6 +1642,7 @@ async function doAddProvider() {
|
|
|
1596
1642
|
S.modelsSig = null;
|
|
1597
1643
|
closeModal();
|
|
1598
1644
|
poll();
|
|
1645
|
+
autoRebindSoon(); // 新增即启用:马上给空链/死链一次推荐机会
|
|
1599
1646
|
if (p && body.api_key) refreshProviderModels(p.id); // 有密钥才自动拉模型列表
|
|
1600
1647
|
} catch (e) {
|
|
1601
1648
|
res.textContent = t("保存失败:") + e.message;
|
|
@@ -1691,7 +1738,7 @@ async function doImport() {
|
|
|
1691
1738
|
btn.textContent = t("完成");
|
|
1692
1739
|
btn.disabled = false;
|
|
1693
1740
|
btn.onclick = closeModal;
|
|
1694
|
-
if (r.imported) { S.selProv = null; S.modelsSig = null; poll(); }
|
|
1741
|
+
if (r.imported) { S.selProv = null; S.modelsSig = null; poll(); autoRebindSoon(); }
|
|
1695
1742
|
} catch (e) {
|
|
1696
1743
|
$("import-result").innerHTML = '<div class="msg bad">' + t("导入失败:") + esc(e.message) + "</div>";
|
|
1697
1744
|
btn.disabled = false; btn.textContent = t("导入选中");
|
|
@@ -1756,18 +1803,39 @@ function renderImplSelects() {
|
|
|
1756
1803
|
}
|
|
1757
1804
|
}
|
|
1758
1805
|
|
|
1759
|
-
async function createTask() {
|
|
1760
|
-
|
|
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");
|
|
1761
1822
|
msg.className = "msg"; msg.textContent = t("提交中…");
|
|
1762
1823
|
const payload = {
|
|
1763
1824
|
type: $("f-type").value,
|
|
1764
1825
|
mode: $("f-mode").value,
|
|
1765
1826
|
title: $("f-title").value.trim(),
|
|
1766
1827
|
goal: $("f-goal").value.trim(),
|
|
1767
|
-
context: $("f-context").value.trim(),
|
|
1768
|
-
workdir: $("f-workdir").value.trim(),
|
|
1769
|
-
};
|
|
1770
|
-
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; // 留空 → 服务端用「默认保存路径」(编排设置可改)
|
|
1771
1839
|
else unhideSideDir(payload.workdir); // 在已移除的目录新建任务 → 自动恢复显示
|
|
1772
1840
|
if (payload.mode === "manual") payload.implementer = $("f-impl").value;
|
|
1773
1841
|
// 附件与代码版本:任务创建时服务端把待提交附件移入 _attachments/ 并注入上下文
|
|
@@ -1785,7 +1853,8 @@ async function createTask() {
|
|
|
1785
1853
|
payload.resume = { agent: resumeAgent, session: sid,
|
|
1786
1854
|
preview: opt ? opt.textContent : "", project: (hit && hit.project) || "" };
|
|
1787
1855
|
}
|
|
1788
|
-
const
|
|
1856
|
+
const flow = flowById(payload.type) || {};
|
|
1857
|
+
const _eng = flow.engine;
|
|
1789
1858
|
if (_eng === "code") {
|
|
1790
1859
|
payload.verify_command = $("f-verify").value.trim();
|
|
1791
1860
|
} else if (_eng === "direct") {
|
|
@@ -1797,11 +1866,18 @@ async function createTask() {
|
|
|
1797
1866
|
const rubric = $("f-rubric").value.trim();
|
|
1798
1867
|
if (rubric) payload.rubric = rubric.split(/[,,、]/).map((s) => s.trim()).filter(Boolean);
|
|
1799
1868
|
const ch = parseInt($("f-chapters").value, 10);
|
|
1800
|
-
if (ch >= 2)
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
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;
|
|
1805
1881
|
const critics = Array.from($("f-critics").querySelectorAll("input:checked")).map((i) => i.value);
|
|
1806
1882
|
if (critics.length) payload.critics = critics;
|
|
1807
1883
|
}
|
|
@@ -1813,11 +1889,14 @@ async function createTask() {
|
|
|
1813
1889
|
try { await refreshState(); } catch (e) { /* 刷失败等轮询兜底 */ }
|
|
1814
1890
|
jumpToRun(r.run_id);
|
|
1815
1891
|
$("f-goal").value = "";
|
|
1892
|
+
if ($("f-bible")) $("f-bible").value = "";
|
|
1816
1893
|
S.atts = []; renderAttachChips(); // 附件已移交任务待提交区,清空本地列表
|
|
1817
|
-
} catch (e) {
|
|
1818
|
-
msg.className = "msg err"; msg.textContent = e.message;
|
|
1819
|
-
}
|
|
1820
|
-
|
|
1894
|
+
} catch (e) {
|
|
1895
|
+
msg.className = "msg err"; msg.textContent = e.message;
|
|
1896
|
+
} finally {
|
|
1897
|
+
resetSubmit();
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1821
1900
|
|
|
1822
1901
|
/* ---------------------------------------------------------- 附件(截图/文件) */
|
|
1823
1902
|
/* 待提交附件:选中/粘贴即上传到服务端 pending 区,创建任务时随 payload 落盘
|
|
@@ -2087,44 +2166,59 @@ function openCtxMenu(x, y, items) {
|
|
|
2087
2166
|
};
|
|
2088
2167
|
}
|
|
2089
2168
|
|
|
2090
|
-
function closeCtxMenu() {
|
|
2091
|
-
const menu = $("ctx-menu");
|
|
2092
|
-
if (menu && !menu.classList.contains("hidden")) menu.classList.add("hidden");
|
|
2093
|
-
}
|
|
2094
|
-
|
|
2095
|
-
|
|
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() {
|
|
2096
2214
|
// 侧栏任务树:右键任务 → 详情/目录/重试/重命名/归档/删除;管理类运行 → 详情/目录/删除记录
|
|
2097
2215
|
$("side-tasks").addEventListener("contextmenu", (e) => {
|
|
2098
2216
|
// .stask 在 .sdir 内部:先判任务行,点到才算文件夹自己的菜单
|
|
2099
|
-
const det = e.target.closest(".stask");
|
|
2100
|
-
if (det) {
|
|
2101
|
-
e.preventDefault();
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
if (runId) items.push({ label: t("打开详情"), fn: () => sideOpenRun(runId) });
|
|
2105
|
-
if (taskId) {
|
|
2106
|
-
items.push("-");
|
|
2107
|
-
items.push({ label: t("打开工作目录"), fn: () => revealPath("tasks", taskId, true) });
|
|
2108
|
-
items.push({ label: t("复制工作目录路径"), fn: () => revealPath("tasks", taskId, false) });
|
|
2109
|
-
if (runId) items.push({ label: t("复制日志目录路径"), fn: () => revealPath("runs", runId, false) });
|
|
2110
|
-
const st = det.dataset.status || "";
|
|
2111
|
-
if (st === "failed" || st === "cancelled") items.push({ label: t("↻ 继续任务"), fn: () => retryTask(taskId) });
|
|
2112
|
-
// 与详情页同一套说法:失败/取消叫「编辑重试」,其余叫「基于此任务新建」
|
|
2113
|
-
items.push({ label: (st === "failed" || st === "cancelled") ? t("✎ 编辑重试") : t("基于此任务新建"),
|
|
2114
|
-
fn: () => newFromTask(taskId) });
|
|
2115
|
-
const sTask = ((S.state || {}).tasks || []).find((x) => x.id === taskId);
|
|
2116
|
-
if (sTask && sTask.serial && st !== "running" && st !== "queued")
|
|
2117
|
-
items.push({ label: t("继续连载(新任务)"), fn: () => continueSerial(taskId) });
|
|
2118
|
-
items.push({ label: t("重命名任务"), fn: () => renameTask(taskId) });
|
|
2119
|
-
items.push({ label: archivedTaskIds().has(taskId) ? t("取消归档") : t("归档"), fn: () => archiveTask(taskId, !archivedTaskIds().has(taskId)) });
|
|
2120
|
-
items.push({ label: t("删除任务"), danger: true, fn: () => deleteTask(taskId) });
|
|
2121
|
-
} else if (runId) {
|
|
2122
|
-
items.push("-");
|
|
2123
|
-
items.push({ label: t("复制日志目录路径"), fn: () => revealPath("runs", runId, false) });
|
|
2124
|
-
items.push({ label: t("删除记录"), danger: true, fn: () => deleteRun(runId) });
|
|
2125
|
-
}
|
|
2126
|
-
openCtxMenu(e.clientX, e.clientY, items);
|
|
2127
|
-
return;
|
|
2217
|
+
const det = e.target.closest(".stask");
|
|
2218
|
+
if (det) {
|
|
2219
|
+
e.preventDefault();
|
|
2220
|
+
openCtxMenu(e.clientX, e.clientY, taskContextItems(det));
|
|
2221
|
+
return;
|
|
2128
2222
|
}
|
|
2129
2223
|
// 文件夹行(ZCode 桌面端式样):新建任务到该目录 / 打开工作目录 / 复制路径 / 移除。
|
|
2130
2224
|
// 「其他」是兜底展示(无主运行的聚合),不是真实目录,不弹菜单。
|
|
@@ -2789,17 +2883,19 @@ function renderSideTasks() {
|
|
|
2789
2883
|
// 二级任务行:单行即全部——点击主栏直开任务详情。
|
|
2790
2884
|
// 内嵌步骤层已砍:步骤/运行明细在任务详情里更全,侧栏只留状态字形 + 徽章 + 相对时间。
|
|
2791
2885
|
// 右键菜单仍吃 data-task / data-run;主栏开着某任务详情时行保持高亮。
|
|
2792
|
-
const row = (g) => {
|
|
2793
|
-
if (g.active) g.status = "running";
|
|
2794
|
-
const sel = (g.key === S.detailTaskKey || g.runIds.indexOf(S.detailRunId) >= 0) ? " active" : "";
|
|
2795
|
-
return '<div class="stask' + sel + (g.archived ? " archived" : "") + '" tabindex="0" role="button" data-key="' + esc(g.key) +
|
|
2796
|
-
'" data-task="' + esc(g.taskId || "") + '" data-run="' + esc(g.runIds[0] || "") +
|
|
2797
|
-
'" data-status="' + esc(g.status || "") + '">' + staskGlyph(g.status) +
|
|
2798
|
-
'<span class="t">' + esc(g.title) + "</span>" +
|
|
2799
|
-
(g.archived ? '<span class="sbadge archb">' + t("已归档") + "</span>" : "") +
|
|
2800
|
-
(g.verdict ? '<span class="sbadge">' + t("待裁决") + "</span>" : "") +
|
|
2801
|
-
'<span class="tm">' + esc(relTime(g.time)) + "</span
|
|
2802
|
-
|
|
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
|
+
};
|
|
2803
2899
|
// 一级文件夹行:折叠箭头 + 目录图标 + 末段名(hover 显全路径)+ 任务计数徽标
|
|
2804
2900
|
box.innerHTML = dirs.map((d) => {
|
|
2805
2901
|
const isOrphan = d.dir === ORPHAN;
|
|
@@ -2949,7 +3045,7 @@ function drawTaskDetail(key, runs) {
|
|
|
2949
3045
|
if (sig === S.taskSig) return;
|
|
2950
3046
|
S.taskSig = sig;
|
|
2951
3047
|
const latest = runs[0]; // runs 新→旧
|
|
2952
|
-
const
|
|
3048
|
+
const ordered = runs.slice(); // 详情按最近运行优先,方便排查
|
|
2953
3049
|
const totalSteps = runs.reduce((a, r) => a + (r.steps || []).length, 0);
|
|
2954
3050
|
const active = runs.some((r) => r.status === "running" || r.status === "queued");
|
|
2955
3051
|
const st = active ? "running" : latest.status;
|
|
@@ -2963,7 +3059,9 @@ function drawTaskDetail(key, runs) {
|
|
|
2963
3059
|
$("btn-retry").classList.toggle("hidden", !(latest.task_id && (latest.status === "failed" || latest.status === "cancelled")));
|
|
2964
3060
|
setEditRetry(latest.task_id, latest.status,
|
|
2965
3061
|
!!((S.state || {}).tasks || []).some((x) => x.id === latest.task_id));
|
|
2966
|
-
S.lastRun = latest;
|
|
3062
|
+
S.lastRun = latest;
|
|
3063
|
+
renderRunOutcome(latest, runs);
|
|
3064
|
+
renderDetailOverview(latest, runs, bmTask);
|
|
2967
3065
|
// 指挥区指向最新的活跃运行(无则隐藏整个指挥区,避免终态任务误导用户以为指令还能生效)
|
|
2968
3066
|
const activeRun = runs.find((r) => r.status === "queued" || r.status === "running");
|
|
2969
3067
|
// 活跃运行(含排队中)显示取消按钮;取消目标钉在活跃 run——任务级详情没有
|
|
@@ -2988,20 +3086,23 @@ function drawTaskDetail(key, runs) {
|
|
|
2988
3086
|
$("rd-plan").classList.add("hidden");
|
|
2989
3087
|
const statusTxt = { queued: t("排队中"), running: t("运行中"), done: t("完成"), failed: t("失败"), cancelled: t("已取消") };
|
|
2990
3088
|
let html = "";
|
|
2991
|
-
|
|
2992
|
-
|
|
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>' +
|
|
2993
3092
|
'<span class="rs-t">' + esc(String(r.created_at || "").slice(5, 16)) + "</span>" +
|
|
2994
3093
|
'<span class="chip ' + esc(r.status || "") + '">' + (statusTxt[r.status] || r.status || "") + "</span>" +
|
|
2995
3094
|
(r.error ? '<span class="rs-err" title="' + esc(r.error.slice(0, 200)) + '">' + esc(r.error.slice(0, 60)) + "</span>" : "") +
|
|
2996
3095
|
"</div>";
|
|
2997
|
-
html += (r.steps || []).map((s) =>
|
|
2998
|
-
'<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 || "") + '">' +
|
|
2999
3098
|
'<span class="n">' + String(s.n).padStart(2, "0") + "</span>" +
|
|
3000
3099
|
'<span class="role">' + esc(s.role) + "</span>" +
|
|
3001
3100
|
'<span class="who">' + esc(t(s.agent_label || s.agent)) + "</span>" +
|
|
3002
3101
|
'<span class="sum">' + esc((s.note ? "◆ " + s.note + " — " : "") + (s.summary || "")) + "</span>" +
|
|
3003
3102
|
'<span class="dur">' + (s.duration_s != null ? s.duration_s + "s" : "") + "</span>" +
|
|
3004
|
-
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>"
|
|
3005
3106
|
).join("");
|
|
3006
3107
|
});
|
|
3007
3108
|
$("rd-steps").innerHTML = html || '<div class="empty">' + t("尚无步骤") + '</div>';
|
|
@@ -3144,7 +3245,7 @@ function applyStepFocus() {
|
|
|
3144
3245
|
el.classList.add("flash");
|
|
3145
3246
|
try { el.scrollIntoView({ block: "center", behavior: "smooth" }); }
|
|
3146
3247
|
catch (e) { el.scrollIntoView(); }
|
|
3147
|
-
if (el.dataset.log) toggleLog(S.detailRunId, el.dataset.log);
|
|
3248
|
+
if (el.dataset.log) toggleLog(el.dataset.runId || S.detailRunId, el.dataset.log);
|
|
3148
3249
|
setTimeout(() => el.classList.remove("flash"), 1800);
|
|
3149
3250
|
}
|
|
3150
3251
|
|
|
@@ -3178,6 +3279,18 @@ function applyRdTabs() {
|
|
|
3178
3279
|
});
|
|
3179
3280
|
document.querySelectorAll("#run-detail .rd-pane").forEach((p) =>
|
|
3180
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
|
+
}
|
|
3181
3294
|
// 对话页签不放日志抽屉:会盖住贴底输入条(手动切来/自动选卡都覆盖)
|
|
3182
3295
|
if (S.rdTab === "chat" && !$("rd-log").classList.contains("hidden")) window.rdLogClose();
|
|
3183
3296
|
renderChatNav();
|
|
@@ -3253,13 +3366,16 @@ function rdTabsSync(ctx) {
|
|
|
3253
3366
|
const sig = (S.detailTaskKey || S.detailRunId || "") + "|" + (ctx.status || "") +
|
|
3254
3367
|
"|" + (ctx.gitState || "") + "|" + (ctx.running ? 1 : 0) + "|" + (chatAvail ? 1 : 0);
|
|
3255
3368
|
if (S.rdTabSig !== sig) {
|
|
3256
|
-
S.rdTabSig = sig;
|
|
3257
|
-
const avail = rdTabAvail();
|
|
3258
|
-
|
|
3259
|
-
//
|
|
3260
|
-
//
|
|
3261
|
-
|
|
3262
|
-
|
|
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)
|
|
3263
3379
|
|| (ctx.running ? (avail.hive ? "hive" : "steps")
|
|
3264
3380
|
: (ctx.gitState === "isolated" && avail.git ? "git"
|
|
3265
3381
|
: (ctx.hasResult && finishing ? "result" : "steps")));
|
|
@@ -3299,7 +3415,9 @@ async function renderRunDetail() {
|
|
|
3299
3415
|
$("btn-continue").classList.toggle("hidden",
|
|
3300
3416
|
!(rcTask && rcTask.serial && run.status !== "running" && run.status !== "queued"));
|
|
3301
3417
|
setEditRetry(run.task_id, run.status, !!rcTask);
|
|
3302
|
-
S.lastRun = run;
|
|
3418
|
+
S.lastRun = run;
|
|
3419
|
+
renderRunOutcome(run);
|
|
3420
|
+
renderDetailOverview(run, [run], rcTask);
|
|
3303
3421
|
const bp = $("btn-pause");
|
|
3304
3422
|
if (bp) { bp.classList.toggle("hidden", !active);
|
|
3305
3423
|
bp.textContent = run.paused ? t("继续执行") : t("暂停"); }
|
|
@@ -3315,16 +3433,20 @@ async function renderRunDetail() {
|
|
|
3315
3433
|
'<span class="stat tasksum hidden" id="rd-meta-task"></span>';
|
|
3316
3434
|
if (S.detailSide) fillMetaTask(S.detailSide.stats || {}); // 缓存命中:轮询重画不闪丢累计组
|
|
3317
3435
|
renderPlan(run);
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
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 || "") + '">' +
|
|
3321
3441
|
'<span class="n">' + String(s.n).padStart(2, "0") + "</span>" +
|
|
3322
3442
|
'<span class="role">' + esc(s.role) + "</span>" +
|
|
3323
|
-
'<span class="who">' + esc(t(s.agent_label || s.agent)) + "</span>" +
|
|
3324
|
-
'<span class="sum">' + esc((s.note ? "◆ " + s.note + " — " : "") + (s.summary || "")) + "</span>" +
|
|
3325
|
-
'<span class="dur">' + (s.duration_s != null ? s.duration_s + "s" : "") + "</span>" +
|
|
3326
|
-
statusChip(s.status) +
|
|
3327
|
-
|
|
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>';
|
|
3328
3450
|
applyStepFocus();
|
|
3329
3451
|
// 报告
|
|
3330
3452
|
if (run.status === "done" || run.report) {
|
|
@@ -4531,9 +4653,10 @@ function bindInspector() {
|
|
|
4531
4653
|
const det = e.target.closest(".stask");
|
|
4532
4654
|
if (det) sideRowActivate(det);
|
|
4533
4655
|
});
|
|
4534
|
-
$("side-tasks").addEventListener("keydown", (e) => {
|
|
4535
|
-
if (e.key !== "Enter" && e.key !== " ") return;
|
|
4536
|
-
|
|
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");
|
|
4537
4660
|
if (det) { e.preventDefault(); sideRowActivate(det); }
|
|
4538
4661
|
});
|
|
4539
4662
|
}
|
|
@@ -4553,14 +4676,13 @@ function artifactsChips(runId, files) {
|
|
|
4553
4676
|
return files.map((f) => {
|
|
4554
4677
|
const isTxt = FP_TXT.has(_fpExt(f.name)); // 文本/代码都有弹窗预览(按代码格式展示)
|
|
4555
4678
|
return '<span class="fc-row">' +
|
|
4556
|
-
'<a class="file-chip" href="/api/runs/' + encodeURIComponent(runId) + "/file?name=" +
|
|
4557
|
-
encodeURIComponent(f.name) + '" target="_blank" rel="noopener" ' +
|
|
4558
|
-
'title="' + esc(f.name + " · " + fmtSize(f.size)) + '"
|
|
4559
|
-
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) + '">' +
|
|
4560
4682
|
'<i class="fx">' + esc(_fpExt(f.name).slice(0, 4) || "file") + "</i>" +
|
|
4561
4683
|
'<span class="p">' + esc(f.name) + "</span><i>" + fmtSize(f.size) + "</i></a>" +
|
|
4562
|
-
(isTxt ? '<a class="file-chip prev" title="' + esc(t("查看内容")) +
|
|
4563
|
-
'"
|
|
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) + '">' +
|
|
4564
4686
|
'<svg class="ico" aria-hidden="true"><use href="#i-book"/></svg>' + t("预览") + "</a>" : "") +
|
|
4565
4687
|
"</span>";
|
|
4566
4688
|
}).join("");
|
|
@@ -4611,12 +4733,14 @@ window.copyText = function (t) {
|
|
|
4611
4733
|
} catch (e) { /* 剪贴板不可用则忽略 */ }
|
|
4612
4734
|
};
|
|
4613
4735
|
|
|
4614
|
-
let currentLog = null;
|
|
4615
|
-
|
|
4616
|
-
/* 当前打开的日志是否仍属于这批步骤(重画后日志路径消失说明步骤被重建,才收起) */
|
|
4617
|
-
function stepsMatch(runs,
|
|
4618
|
-
|
|
4619
|
-
|
|
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
|
+
}
|
|
4620
4744
|
|
|
4621
4745
|
function renderPlan(run) {
|
|
4622
4746
|
const box = $("rd-plan");
|
|
@@ -4677,22 +4801,24 @@ window.rdLogClose = function () {
|
|
|
4677
4801
|
if (st) st.textContent = "";
|
|
4678
4802
|
};
|
|
4679
4803
|
|
|
4680
|
-
async function toggleLog(runId, rel) {
|
|
4681
|
-
const box = $("rd-log"), pre = $("rd-log-text");
|
|
4682
|
-
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")) {
|
|
4683
4807
|
window.rdLogClose(); return;
|
|
4684
4808
|
}
|
|
4685
4809
|
try {
|
|
4686
4810
|
const r = await api("/api/runs/" + encodeURIComponent(runId) + "/log?step=" + encodeURIComponent(rel) + "&pretty=1");
|
|
4687
4811
|
// 拉取期间可能切详情:过期日志不落盘(A 的步骤输出写进 B 详情的抽屉)。
|
|
4688
4812
|
// 检查器蜂巢/步骤点开日志同款:run 属检查器当前 run 也算在场
|
|
4689
|
-
const owned = S.detailRunId === runId ||
|
|
4690
|
-
|
|
4691
|
-
|
|
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;
|
|
4692
4818
|
if (!owned) return;
|
|
4693
4819
|
pre.textContent = r.log || t("(等待输出…)");
|
|
4694
4820
|
box.classList.remove("hidden");
|
|
4695
|
-
currentLog = rel;
|
|
4821
|
+
currentLog = { runId, rel };
|
|
4696
4822
|
const st = $("rd-log-step");
|
|
4697
4823
|
if (st) st.textContent = rdLogStepLabel(runId, rel);
|
|
4698
4824
|
pre.scrollTop = pre.scrollHeight; // 打开即看最新输出
|
|
@@ -4702,10 +4828,10 @@ async function toggleLog(runId, rel) {
|
|
|
4702
4828
|
stopLogLive();
|
|
4703
4829
|
if (!live) return;
|
|
4704
4830
|
S.logLive = setInterval(async () => {
|
|
4705
|
-
if (!currentLog || currentLog !== rel) return stopLogLive();
|
|
4831
|
+
if (!currentLog || currentLog.runId !== runId || currentLog.rel !== rel) return stopLogLive();
|
|
4706
4832
|
try {
|
|
4707
4833
|
const rr = await api("/api/runs/" + encodeURIComponent(runId) + "/log?step=" + encodeURIComponent(rel) + "&pretty=1");
|
|
4708
|
-
if (currentLog === rel) {
|
|
4834
|
+
if (currentLog && currentLog.runId === runId && currentLog.rel === rel) {
|
|
4709
4835
|
// 贴底跟随:用户滚到底部附近才自动滚到最新输出,回看历史不打扰
|
|
4710
4836
|
const stick = pre.scrollHeight - pre.scrollTop - pre.clientHeight < 48;
|
|
4711
4837
|
pre.textContent = rr.log || t("(等待输出…)");
|
|
@@ -4789,8 +4915,16 @@ window.renderHive = function (run) {
|
|
|
4789
4915
|
});
|
|
4790
4916
|
const running = steps.filter((s) => s.status === "running");
|
|
4791
4917
|
const sub = $("rd-hive-sub");
|
|
4792
|
-
if (sub)
|
|
4793
|
-
|
|
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
|
+
}
|
|
4794
4928
|
const activeIdx = lanes.map((l) => byStage[l].some((s) => s.status === "running")).lastIndexOf(true);
|
|
4795
4929
|
const cell = (s, laneIdx, cellIdx) => {
|
|
4796
4930
|
// 五归一:取消/超时的格子不再冒充「完成」——与步骤芯片同三色体系
|
|
@@ -4805,16 +4939,19 @@ window.renderHive = function (run) {
|
|
|
4805
4939
|
const key = run.id + "|" + (s.log || "");
|
|
4806
4940
|
if (st !== "running") delete hiveTails[key];
|
|
4807
4941
|
const tailText = st === "running" ? (hiveTails[key] || concl) : concl;
|
|
4808
|
-
const title = [s.role, who,
|
|
4942
|
+
const title = [s.role, who,
|
|
4943
|
+
(s.duration_s != null ? s.duration_s + "s" : ""),
|
|
4944
|
+
s.started_at ? t("开始于 ") + s.started_at : "",
|
|
4809
4945
|
(s.note ? "◆ " + s.note : "")].filter(Boolean).join(" · ")
|
|
4810
4946
|
+ (concl && st !== "running" ? "\n" + t("结论:") + concl : "");
|
|
4811
4947
|
// --d:入场瀑布逐格延迟(泳道间 110ms + 同泳道逐格 45ms),样式端消费
|
|
4812
|
-
return '<div class="hive-cell st-' + st + (st === "running" ? " hc-breathe" : "") + '"' +
|
|
4813
|
-
'
|
|
4814
|
-
'
|
|
4815
|
-
'
|
|
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
|
+
'>' +
|
|
4816
4953
|
'<div class="hc-head">' +
|
|
4817
|
-
'<
|
|
4954
|
+
'<img class="hc-bee" src="icons/bee.svg" alt="" aria-hidden="true">' +
|
|
4818
4955
|
'<span class="hc-role">' + esc(s.role || "") + "</span>" +
|
|
4819
4956
|
'<span class="hc-who">' + esc(who) + "</span></div>" +
|
|
4820
4957
|
'<div class="hc-tail" data-log="' + esc(s.log || "") + '">' +
|
|
@@ -4822,9 +4959,10 @@ window.renderHive = function (run) {
|
|
|
4822
4959
|
'<div class="hc-meta"><span class="hc-elapsed" data-started="' + esc(s.started_at || "") + '">' +
|
|
4823
4960
|
(s.duration_s != null ? s.duration_s + "s" : hiveElapsed(s.started_at)) + "</span>" +
|
|
4824
4961
|
(st === "running" ? '<span class="hc-live"><i class="live-dot"></i>' + t("工作中") + "</span>" : "") +
|
|
4825
|
-
(st === "timeout" ? '<span class="hc-dead">⏱ ' + t("超时") + "</span>" : "") +
|
|
4826
|
-
(st === "cancelled" ? '<span class="hc-dead">' + t("已取消") + "</span>" : "") +
|
|
4827
|
-
"</
|
|
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>";
|
|
4828
4966
|
};
|
|
4829
4967
|
const dots = (list, laneIdx) => list.slice(-12).map((s, di) => {
|
|
4830
4968
|
const dc = s.status === "running" ? "run" : s.status === "failed" ? "fail"
|
|
@@ -4843,16 +4981,24 @@ window.renderHive = function (run) {
|
|
|
4843
4981
|
: (activeIdx === -1 || i < activeIdx ? "lane-done" : "lane-idle");
|
|
4844
4982
|
// 泳道头视觉 v2:阶段序号徽章(01/02…流水线站序)+ 已完成进度(settled/total)
|
|
4845
4983
|
const settled = list.filter((s) => !["running", "queued"].includes(s.status)).length;
|
|
4984
|
+
const meter = list.length ? Math.round((settled / list.length) * 100) : 0;
|
|
4846
4985
|
return '<div class="hive-lane ' + cls + '">' +
|
|
4847
4986
|
'<div class="lane-head">' +
|
|
4848
4987
|
'<span class="lane-idx">' + String(i + 1).padStart(2, "0") + "</span>" +
|
|
4849
4988
|
'<span class="lane-name">' + esc(stage) + '</span>' +
|
|
4850
4989
|
'<span class="lane-n">×' + list.length + "</span>" +
|
|
4851
4990
|
(settled ? '<span class="lane-prog">' + settled + "/" + list.length + "</span>" : "") +
|
|
4991
|
+
'<span class="lane-meter" aria-label="' + meter + '%"><i style="width:' + meter + '%"></i></span>' +
|
|
4852
4992
|
(hasRun ? '<span class="lane-live"><i class="live-dot"></i>' + t("进行中") + "</span>" : "") + "</div>" +
|
|
4853
4993
|
'<div class="lane-track">' + dots(list, i) + "</div>" +
|
|
4854
4994
|
'<div class="lane-cells">' + list.slice(-8).map((s, ci) => cell(s, i, ci)).join("") + "</div></div>";
|
|
4855
|
-
}).join('<div class="lane-flow" aria-hidden="true"
|
|
4995
|
+
}).join('<div class="lane-flow" aria-hidden="true">'
|
|
4996
|
+
+ '<svg class="flow-line flow-line-down" viewBox="0 0 36 44" aria-hidden="true">'
|
|
4997
|
+
+ '<path class="flow-path" d="M14 2 C14 16 22 28 22 42"/><path class="flow-tip" d="M16 36 L22 43 L28 36"/></svg>'
|
|
4998
|
+
+ '<svg class="flow-line flow-line-up" viewBox="0 0 36 44" aria-hidden="true">'
|
|
4999
|
+
+ '<path class="flow-path" d="M22 2 C22 16 14 28 14 42"/><path class="flow-tip" d="M8 36 L14 43 L20 36"/></svg>'
|
|
5000
|
+
+ '<span class="flow-bee"><img src="icons/bee.svg" alt="" aria-hidden="true"></span>'
|
|
5001
|
+
+ '</div>');
|
|
4856
5002
|
// 入场瀑布闸门:只按"结构"(泳道数/格数/状态构成)签名;运行中尾巴/秒表每 2s
|
|
4857
5003
|
// 变化不能触重播。签名没变就不动 DOM——格子不闪、动画不重启、悬停不弹手。
|
|
4858
5004
|
const sig = lanes.map((l) => l + ":" + byStage[l].length + "(" +
|
|
@@ -5144,9 +5290,10 @@ async function renderChat(run, active) {
|
|
|
5144
5290
|
if (sig === chatSig) return; // 轮询重画去抖:内容没变不重建 DOM(保住输入焦点)
|
|
5145
5291
|
const runForFetch = run.id; // 拉取期间可能切详情:过期响应不落盘(同 renderRunDetail 闸门)
|
|
5146
5292
|
chatSig = sig;
|
|
5147
|
-
let
|
|
5148
|
-
try {
|
|
5149
|
-
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) || [];
|
|
5150
5297
|
if (!$("run-detail") || $("run-detail").classList.contains("hidden")) return;
|
|
5151
5298
|
// 过期闸门:run 级(renderRunDetail)与任务级(drawTaskDetail)都经 S.lastRun
|
|
5152
5299
|
// 指向当前详情正在展示的那条 run——拉取期间切了详情就丢弃响应
|
|
@@ -5159,7 +5306,7 @@ async function renderChat(run, active) {
|
|
|
5159
5306
|
const attName = (a) => { const p = typeof a === "string" ? a
|
|
5160
5307
|
: ((a && (a.name || a.path)) || "");
|
|
5161
5308
|
return String(p).split(/[\\/]/).pop() || ""; };
|
|
5162
|
-
|
|
5309
|
+
let html = items.map((it) => {
|
|
5163
5310
|
if (it.kind === "user") {
|
|
5164
5311
|
// 用户消息:右侧浅灰圆角块,元信息在块内顶部。
|
|
5165
5312
|
// 附件胶囊可点开预览:data-att 带 _attachments/ 相对路径(老数据裸文件名
|
|
@@ -5195,21 +5342,183 @@ async function renderChat(run, active) {
|
|
|
5195
5342
|
// 元信息只留 名字+时间(+失败标记):路由/供应商细节属于「步骤」页签,塞这里就是噪音
|
|
5196
5343
|
const metaBad = it.status === "failed" || it.status === "cancelled";
|
|
5197
5344
|
// 智能体回复:头像 + 整幅正文(不套气泡框),元信息小字落在正文下方
|
|
5198
|
-
return '<div class="chat-row">' +
|
|
5199
|
-
'<span class="chat-avatar" aria-hidden="true"><svg class="ico"><use href="#i-bee"></use></svg></span>' +
|
|
5200
|
-
'<div class="chat-bubble agent">' +
|
|
5201
|
-
'<div class="chat-body">' + bodyHtml + "</div>" +
|
|
5202
|
-
'<div class="chat-meta">' + esc(it.who || "") + " " + esc(chatTime(it.at)) +
|
|
5203
|
-
(metaBad ? " · " + t(it.status === "failed" ? "失败" : "已取消") : "") + "</div
|
|
5204
|
-
|
|
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);
|
|
5205
5360
|
const hint = $("rd-chat-hint");
|
|
5206
5361
|
if (hint) hint.textContent = active
|
|
5207
5362
|
? t("运行中:新消息会排队,本轮回答完后依次送达")
|
|
5208
5363
|
: t("已结束:发送后将自动开新一轮接着做");
|
|
5364
|
+
flow.innerHTML = html || '<div class="hint">' + esc(t("还没有对话内容")) + "</div>";
|
|
5209
5365
|
flow.scrollTop = flow.scrollHeight;
|
|
5210
5366
|
}
|
|
5211
5367
|
|
|
5212
|
-
|
|
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() {
|
|
5213
5522
|
const box = $("rd-chat-att-list");
|
|
5214
5523
|
if (!box) return;
|
|
5215
5524
|
// 点名字开预览(待提交区原文走 /api/attachments/<id>),× 才是移除
|
|
@@ -5324,8 +5633,16 @@ function bindChat() {
|
|
|
5324
5633
|
// 附件胶囊点击预览:委托绑在静态容器上——时间线轮询整块重建 innerHTML、
|
|
5325
5634
|
// 输入条胶囊增删重画,都不丢监听。时间线胶囊走已落盘文件,输入条胶囊
|
|
5326
5635
|
// (× 移除按钮除外)走待提交区
|
|
5327
|
-
$("rd-chat-flow").addEventListener("click", (e) => {
|
|
5328
|
-
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");
|
|
5329
5646
|
if (chip) window.chatAttPopup(chip.dataset.att);
|
|
5330
5647
|
});
|
|
5331
5648
|
const attBox = $("rd-chat-att-list");
|
|
@@ -5761,20 +6078,20 @@ function bindSelById(id) {
|
|
|
5761
6078
|
function bindRepaint() { S.bindSig = null; renderBindings(); }
|
|
5762
6079
|
|
|
5763
6080
|
/* 一键推荐绑定:给每个链为空的 CLI 按协议适配规则预填一条主模型。
|
|
5764
|
-
* 推荐顺序:显式协议原生匹配 > auto 且 wire_caps
|
|
5765
|
-
*
|
|
5766
|
-
*
|
|
6081
|
+
* 推荐顺序:显式协议原生匹配 > auto 且 wire_caps 命中;同分按 priority /
|
|
6082
|
+
* 数组序稳定。默认模型停用的厂商直接不推荐——它当前键位就是坏的,绑上去
|
|
6083
|
+
* 解析照样失败;没有合适的就什么都不绑(2026-09-17 用户拍板,不再保留
|
|
6084
|
+
* 「只剩它也上」的兜底)。只预填草稿态(dirty),逐条看清后各自保存
|
|
5767
6085
|
* 或「全部保存」,健康链绝不被覆盖。 */
|
|
5768
6086
|
function recommendFor(c) {
|
|
5769
|
-
|
|
5770
|
-
|
|
6087
|
+
// 目录页「默认模型」下拉用 orch_kind || id 判协议(仅管理条目没有 orch_kind),
|
|
6088
|
+
// 推荐口径与下拉过滤保持一致;绑定页条目恒有 orch_kind,不受影响
|
|
6089
|
+
const allow = bindAllowedProtocols(c.orch_kind || c.id);
|
|
6090
|
+
const badDefault = (p) => (p.models || []).some((m) => m.name === p.model &&
|
|
6091
|
+
(m.hidden || m.enabled === false));
|
|
6092
|
+
const provs = (S.providers || []).filter((p) => !badDefault(p) && chainProvUsable(p, allow));
|
|
5771
6093
|
if (!provs.length) return null;
|
|
5772
|
-
const score = (p) =>
|
|
5773
|
-
const native = allow.indexOf(p.protocol) >= 0;
|
|
5774
|
-
const badDefault = (p.models || []).some((m) => m.name === p.model &&
|
|
5775
|
-
(m.hidden || m.enabled === false)) ? 500 : 0;
|
|
5776
|
-
return (native ? 0 : 1) * 1000 + badDefault + (p.priority || 0);
|
|
5777
|
-
};
|
|
6094
|
+
const score = (p) => (allow.indexOf(p.protocol) >= 0 ? 0 : 1) * 1000 + (p.priority || 0);
|
|
5778
6095
|
provs.sort((a, b) => score(a) - score(b));
|
|
5779
6096
|
const p = provs[0];
|
|
5780
6097
|
const models = (p.models || []).filter((m) => !m.hidden && m.enabled !== false);
|
|
@@ -5812,48 +6129,38 @@ function chainDeadReasons(c, chain) {
|
|
|
5812
6129
|
return dead;
|
|
5813
6130
|
}
|
|
5814
6131
|
|
|
6132
|
+
/* 单条绑定链的推荐修复动作:需要改返回新链,不动返回 null。
|
|
6133
|
+
* 空链 → 预填推荐;整条死透(或只有链首且已死)→ 重推荐;链首死但链内还有
|
|
6134
|
+
* 活的备选 → 新链首插最前(原降级序保留,推荐项已在链内则升首去重)。 */
|
|
6135
|
+
function bindRepairAction(c, st) {
|
|
6136
|
+
const rec = recommendFor(c);
|
|
6137
|
+
if (!st.chain.length) return rec ? [rec] : null;
|
|
6138
|
+
const dead = chainDeadReasons(c, st.chain);
|
|
6139
|
+
const allDead = dead.every((d) => d);
|
|
6140
|
+
const headDead = dead[0] !== ""; // 空 p = CLI 默认凭据,是有意配置不算死
|
|
6141
|
+
if (allDead) return rec ? [rec] : null;
|
|
6142
|
+
if (headDead && rec && chainKey(st.chain) !== chainKey([rec]))
|
|
6143
|
+
return [rec].concat(st.chain.filter((x) => !(x.p === rec.p && x.m === rec.m)));
|
|
6144
|
+
return null;
|
|
6145
|
+
}
|
|
6146
|
+
|
|
5815
6147
|
function autoBindAll() {
|
|
5816
6148
|
const targets = (S.catalog || []).filter((c) => c.installed && c.orch_kind);
|
|
5817
6149
|
let filled = 0, skipped = 0, noProv = 0, refilled = 0;
|
|
5818
6150
|
for (const c of targets) {
|
|
5819
6151
|
const st = bindSelById(c.id);
|
|
5820
|
-
|
|
5821
|
-
|
|
5822
|
-
|
|
5823
|
-
st.chain
|
|
5824
|
-
|
|
5825
|
-
|
|
6152
|
+
const act = bindRepairAction(c, st);
|
|
6153
|
+
if (!act) {
|
|
6154
|
+
// 没动它:分清「健康链无需推荐」和「没有可推荐的」两种落空
|
|
6155
|
+
if (!st.chain.length) { noProv++; continue; }
|
|
6156
|
+
const dead = chainDeadReasons(c, st.chain);
|
|
6157
|
+
if (dead.every((d) => d)) noProv++; else skipped++;
|
|
5826
6158
|
continue;
|
|
5827
6159
|
}
|
|
5828
|
-
|
|
5829
|
-
|
|
5830
|
-
|
|
5831
|
-
|
|
5832
|
-
const allDead = aliveN === 0;
|
|
5833
|
-
const headDead = dead[0] !== ""; // 空 p = CLI 默认凭据,是有意配置不算死
|
|
5834
|
-
const rec = recommendFor(c);
|
|
5835
|
-
if (allDead && rec) {
|
|
5836
|
-
st.chain = [rec];
|
|
5837
|
-
st.dirty = true;
|
|
5838
|
-
refilled++;
|
|
5839
|
-
} else if (allDead) {
|
|
5840
|
-
noProv++;
|
|
5841
|
-
} else if (headDead && rec && chainKey(st.chain) !== chainKey([rec])) {
|
|
5842
|
-
// 链首死但链内还有活的备选:只把推荐的新链首插到最前面(原降级序保留)
|
|
5843
|
-
const hasRec = st.chain.some((x) => x.p === rec.p && x.m === rec.m);
|
|
5844
|
-
if (!hasRec) {
|
|
5845
|
-
st.chain = [rec].concat(st.chain);
|
|
5846
|
-
st.dirty = true;
|
|
5847
|
-
refilled++;
|
|
5848
|
-
} else {
|
|
5849
|
-
// 推荐项已在链内:把它升到链首
|
|
5850
|
-
st.chain = [rec].concat(st.chain.filter((x) => x !== rec));
|
|
5851
|
-
st.dirty = true;
|
|
5852
|
-
refilled++;
|
|
5853
|
-
}
|
|
5854
|
-
} else {
|
|
5855
|
-
skipped++;
|
|
5856
|
-
}
|
|
6160
|
+
const wasEmpty = !st.chain.length;
|
|
6161
|
+
st.chain = act;
|
|
6162
|
+
st.dirty = true;
|
|
6163
|
+
if (wasEmpty) filled++; else refilled++;
|
|
5857
6164
|
}
|
|
5858
6165
|
bindRepaint();
|
|
5859
6166
|
if (filled && refilled) {
|
|
@@ -5871,6 +6178,56 @@ function autoBindAll() {
|
|
|
5871
6178
|
}
|
|
5872
6179
|
}
|
|
5873
6180
|
|
|
6181
|
+
/* 厂商/模型停用·启用·删除后自动补一次推荐绑定(2026-09-17 用户拍板):
|
|
6182
|
+
* 绑定链——空链预填、死链重推荐、死链首插新首,与「一键推荐绑定」同规则,
|
|
6183
|
+
* 但直接落盘(自动场景没有人工确认环节);目录页——空默认模型直填。
|
|
6184
|
+
* 没有合适的推荐就保持原样,什么都不绑。绑定页上用户手改中的草稿(dirty)
|
|
6185
|
+
* 不碰;一处都没改成静默返回,不打扰停用/启用的操作反馈。 */
|
|
6186
|
+
let _autoRebindRunning = false, _autoRebindAgain = false;
|
|
6187
|
+
async function autoRebindSoon() {
|
|
6188
|
+
if (_autoRebindRunning) { _autoRebindAgain = true; return; }
|
|
6189
|
+
_autoRebindRunning = true;
|
|
6190
|
+
try {
|
|
6191
|
+
await poll(); // 拿停用/启用落盘后的最新 providers / bindings / catalog
|
|
6192
|
+
let fixed = 0;
|
|
6193
|
+
for (const c of (S.catalog || []).filter((x) => x.installed && x.orch_kind)) {
|
|
6194
|
+
const st = bindSelById(c.id);
|
|
6195
|
+
if (st.dirty) continue; // 用户手改中,不覆盖草稿
|
|
6196
|
+
const act = bindRepairAction(c, st);
|
|
6197
|
+
if (!act) continue;
|
|
6198
|
+
const b = (S.bindings || {})[c.id] || {};
|
|
6199
|
+
try {
|
|
6200
|
+
await api("/api/models/binding", { method: "POST", body: JSON.stringify({
|
|
6201
|
+
agent_id: c.id, provider_id: act[0].p,
|
|
6202
|
+
chain: act.map((x) => ({ provider_id: x.p, model: x.m })),
|
|
6203
|
+
difficulty_routing: !!b.difficulty_routing }) });
|
|
6204
|
+
st.chain = act; st.dirty = false; st.key = chainKey(act);
|
|
6205
|
+
fixed++;
|
|
6206
|
+
} catch (e) { /* 单条失败不打断,等下次变更再补 */ }
|
|
6207
|
+
}
|
|
6208
|
+
for (const c of (S.catalog || []).filter((x) =>
|
|
6209
|
+
x.installed && x.config_writable && !fmtModel(x.model))) {
|
|
6210
|
+
const rec = recommendFor(c);
|
|
6211
|
+
if (!rec) continue;
|
|
6212
|
+
try {
|
|
6213
|
+
const r = await api("/api/catalog/" + encodeURIComponent(c.id) + "/model",
|
|
6214
|
+
{ method: "POST", body: JSON.stringify({ model: rec.m }) });
|
|
6215
|
+
c.model = r.model || rec.m;
|
|
6216
|
+
fixed++;
|
|
6217
|
+
} catch (e) { /* 同上 */ }
|
|
6218
|
+
}
|
|
6219
|
+
if (fixed) {
|
|
6220
|
+
S.catSig = null; S.bindSig = null;
|
|
6221
|
+
render();
|
|
6222
|
+
toast(t("厂商/模型变动,已自动重绑 %1 处。").replace("%1", fixed));
|
|
6223
|
+
}
|
|
6224
|
+
} catch (e) { /* 自动补绑失败静默:不打断用户的停用/启用操作 */ }
|
|
6225
|
+
finally {
|
|
6226
|
+
_autoRebindRunning = false;
|
|
6227
|
+
if (_autoRebindAgain) { _autoRebindAgain = false; autoRebindSoon(); }
|
|
6228
|
+
}
|
|
6229
|
+
}
|
|
6230
|
+
|
|
5874
6231
|
async function saveAllBindings() {
|
|
5875
6232
|
const targets = (S.catalog || []).filter((c) => c.installed && c.orch_kind);
|
|
5876
6233
|
const dirty = targets.filter((c) => (S.bindSel[c.id] || {}).dirty);
|
|
@@ -5916,7 +6273,7 @@ function bindModelBox(c, provId) {
|
|
|
5916
6273
|
"</span>";
|
|
5917
6274
|
}).join("")
|
|
5918
6275
|
: '<span class="hint">' + t("未设置") + (provId
|
|
5919
|
-
? t("
|
|
6276
|
+
? t("(按供应商/难度自动解析——供应商协议不匹配或被停用时解析为空,相关步骤将判失败)")
|
|
5920
6277
|
: t("(用 CLI 默认模型——不会注入任何供应商凭据)")) + "</span>";
|
|
5921
6278
|
return '<div class="field"><label>' + t("运行时模型链(跨厂商,最多 ") + MAX_ORCH_MODELS + t(" 条)") + "</label>" +
|
|
5922
6279
|
'<div class="orch-row">' + chips +
|
|
@@ -6106,6 +6463,45 @@ async function saveModel(id) {
|
|
|
6106
6463
|
poll();
|
|
6107
6464
|
}
|
|
6108
6465
|
|
|
6466
|
+
/* 一键绑定推荐模型(智能体目录页):给「默认模型」还空着的已安装智能体按
|
|
6467
|
+
* 协议适配规则(复用绑定页 recommendFor 评分)挑一家厂商的启用模型并直接
|
|
6468
|
+
* 写入该 CLI 自身配置(写入侧先自动 .bak 备份)。已配置的不动——目录页的
|
|
6469
|
+
* 默认模型会写进 CLI 全局配置,手工设置过的值(含 CLI 订阅自带的默认)不
|
|
6470
|
+
* 该被一键覆盖;要换模型逐卡下拉改就行。 */
|
|
6471
|
+
async function catAutoBindAll() {
|
|
6472
|
+
const targets = (S.catalog || []).filter((c) => c.installed && c.config_writable);
|
|
6473
|
+
if (!targets.length) { toast(t("没有支持写入默认模型的已安装智能体。"), true); return; }
|
|
6474
|
+
const empty = targets.filter((c) => !fmtModel(c.model));
|
|
6475
|
+
if (!empty.length) { toast(t("所有已安装智能体都已配置默认模型,无需绑定。")); return; }
|
|
6476
|
+
let done = 0, failed = 0, noProv = 0, firstErr = "";
|
|
6477
|
+
for (const c of empty) {
|
|
6478
|
+
const rec = recommendFor(c);
|
|
6479
|
+
if (!rec) { noProv++; continue; }
|
|
6480
|
+
try {
|
|
6481
|
+
const r = await api("/api/catalog/" + encodeURIComponent(c.id) + "/model",
|
|
6482
|
+
{ method: "POST", body: JSON.stringify({ model: rec.m }) });
|
|
6483
|
+
c.model = r.model || rec.m;
|
|
6484
|
+
done++;
|
|
6485
|
+
} catch (e) {
|
|
6486
|
+
failed++;
|
|
6487
|
+
if (!firstErr) firstErr = c.name + ":" + e.message;
|
|
6488
|
+
}
|
|
6489
|
+
}
|
|
6490
|
+
S.catSig = null;
|
|
6491
|
+
renderCatalog();
|
|
6492
|
+
if (done && failed) {
|
|
6493
|
+
toast(t("已为 %1 个智能体写入推荐模型,%2 个失败。").replace("%1", done).replace("%2", failed) +
|
|
6494
|
+
(firstErr ? " " + firstErr : ""), true);
|
|
6495
|
+
} else if (done) {
|
|
6496
|
+
toast(t("已为 %1 个智能体写入推荐模型(原配置已自动备份 .bak)。").replace("%1", done));
|
|
6497
|
+
} else if (failed) {
|
|
6498
|
+
toast(t("推荐模型写入失败:") + firstErr, true);
|
|
6499
|
+
} else {
|
|
6500
|
+
toast(t("没有可推荐的:先到「模型接入」页导入与 CLI 协议匹配的供应商。"), true);
|
|
6501
|
+
}
|
|
6502
|
+
poll();
|
|
6503
|
+
}
|
|
6504
|
+
|
|
6109
6505
|
/* 一键打开:web 类后台起服务并自动开浏览器;console 类新开终端窗口跑交互 TUI。
|
|
6110
6506
|
用的模型就是目录页「默认模型」已写入该 CLI 配置文件的那份;密钥由服务端按编排同款规则注入 */
|
|
6111
6507
|
async function openAgent(id) {
|
|
@@ -7255,7 +7651,8 @@ async function suApply() {
|
|
|
7255
7651
|
clearInterval(SU_TIMER);
|
|
7256
7652
|
SU_TIMER = setInterval(async () => {
|
|
7257
7653
|
try {
|
|
7258
|
-
const
|
|
7654
|
+
const d = await api("/api/runs/" + encodeURIComponent(runId));
|
|
7655
|
+
const run = d && d.run; // /api/runs/<id> 返回 {run:{…}} 信封,别读外壳
|
|
7259
7656
|
if (run && run.status && run.status !== "running" && run.status !== "queued") {
|
|
7260
7657
|
clearInterval(SU_TIMER); SU_TIMER = null;
|
|
7261
7658
|
$("su-apply").disabled = false;
|
|
@@ -7587,18 +7984,31 @@ function usageMultilineSvg(byDay, byDayModel) {
|
|
|
7587
7984
|
"<title>" + esc(days[i] + "\n" + s.name + " " + fmtTok(s.vals[i]) + t(" tokens")) + "</title></circle>";
|
|
7588
7985
|
});
|
|
7589
7986
|
});
|
|
7590
|
-
const grid = [0.25, 0.5, 0.75].map((f) =>
|
|
7591
|
-
'<line class="uc-grid" x1="' + padL + '" y1="' + (padT + ih * f).toFixed(1) +
|
|
7592
|
-
'" x2="' + (W - padR) + '" y2="' + (padT + ih * f).toFixed(1) + '"/>').join("") +
|
|
7593
|
-
'<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("");
|
|
7594
8004
|
const legend = '<div class="um-legend">' +
|
|
7595
8005
|
series.map((s, si) =>
|
|
7596
8006
|
'<span><i style="background:' + usageColor(si) + '"></i>' +
|
|
7597
8007
|
esc(s.name.length > 22 ? s.name.slice(0, 21) + "…" : s.name) + "</span>").join("") +
|
|
7598
8008
|
'<span class="um-peak">' + esc(t("峰值 ") + fmtTok(max)) + "</span></div>";
|
|
7599
|
-
return legend +
|
|
7600
|
-
'<svg class="usage-svg" viewBox="0 0 ' + W + " " + H + '" role="img" preserveAspectRatio="xMidYMid meet">' +
|
|
7601
|
-
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>";
|
|
7602
8012
|
}
|
|
7603
8013
|
|
|
7604
8014
|
/* Token 活动热力图:GitHub 贡献图风格。窗口自适应——覆盖全部活跃历史(封顶
|
|
@@ -7829,9 +8239,10 @@ function renderUsage() {
|
|
|
7829
8239
|
// 不提示的话「输入 0 · 输出 0」会被误读成统计坏了
|
|
7830
8240
|
const noBreakdown = tot.tokens > 0 && !tot.input && !tot.output && !tot.cached;
|
|
7831
8241
|
$("usage-kpis").innerHTML = [
|
|
7832
|
-
kpiCard(t("累计 Token 数"), fmtTok(tot.tokens),
|
|
7833
|
-
t("调用 ") + fmtTok(tot.calls) + t(" 次 · 成功率 ") +
|
|
7834
|
-
(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),
|
|
7835
8246
|
true, "i-sigma"),
|
|
7836
8247
|
kpiCard(t("峰值 Token 数"), fmtTok(tot.peak_tokens),
|
|
7837
8248
|
t("单日最高 · 日均 ") + fmtTok(activeDays ? tot.tokens / activeDays : 0), false, "i-gauge"),
|
|
@@ -8105,9 +8516,47 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
8105
8516
|
b.addEventListener("click", () => setUsageHeatMode(b.dataset.heat)));
|
|
8106
8517
|
$("btn-back").addEventListener("click", closeRun);
|
|
8107
8518
|
$("btn-cancel").addEventListener("click", cancelRun);
|
|
8108
|
-
bindDirector();
|
|
8109
|
-
bindChat();
|
|
8110
|
-
|
|
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
|
+
// 详情标签页:手点即切并钉住——状态变化触发的自动选卡不再抢用户的手选
|
|
8111
8560
|
$("rd-tabs").addEventListener("click", (e) => {
|
|
8112
8561
|
const b = e.target.closest(".rd-tab");
|
|
8113
8562
|
if (!b || b.classList.contains("hidden")) return;
|
|
@@ -8115,6 +8564,21 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
8115
8564
|
S.rdTabPin = true;
|
|
8116
8565
|
applyRdTabs();
|
|
8117
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
|
+
});
|
|
8118
8582
|
// 详情子页显隐 → 容器宽度 class(:has 在部分浏览器不生效,JS 同步为准)
|
|
8119
8583
|
(function () {
|
|
8120
8584
|
const sub = document.getElementById("sub-runs");
|
|
@@ -8258,6 +8722,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
8258
8722
|
$("btn-reload-catalog").addEventListener("click", async () => {
|
|
8259
8723
|
await api("/api/catalog/reload", { method: "POST" }); poll(); refreshSessionAgents();
|
|
8260
8724
|
});
|
|
8725
|
+
$("btn-catalog-autobind").addEventListener("click", catAutoBindAll);
|
|
8261
8726
|
$("btn-import").addEventListener("click", openImportDialog);
|
|
8262
8727
|
$("btn-add-provider").addEventListener("click", openAddProviderDialog);
|
|
8263
8728
|
$("prov-search").addEventListener("input", () => {
|