codebee 0.1.4 → 0.1.6
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 +44 -0
- package/README.md +8 -0
- package/app/core/attachments.py +40 -0
- package/app/core/bookmeta.py +157 -52
- package/app/core/bookmeta_catalog.py +67 -90
- package/app/core/builtin_agent.py +190 -8
- package/app/core/flows.py +328 -328
- package/app/core/gitmod.py +82 -8
- package/app/core/health.py +55 -1
- package/app/core/history.py +8 -2
- package/app/core/jobs.py +19 -4
- package/app/core/manager.py +159 -1
- package/app/core/modelhub.py +148 -15
- package/app/core/pipeline.py +2392 -2267
- package/app/core/registry.py +4 -0
- package/app/core/router.py +13 -7
- package/app/core/runner.py +194 -21
- package/app/core/selfupdate.py +60 -11
- package/app/core/store.py +4 -2
- package/app/main.py +101 -7
- package/app/ui/app.js +447 -115
- package/app/ui/i18n.js +32 -10
- package/app/ui/icons/bee.svg +79 -0
- package/app/ui/index.html +8 -3
- package/app/ui/style.css +3226 -2764
- package/package.json +2 -1
package/app/ui/app.js
CHANGED
|
@@ -488,10 +488,12 @@ function fmtModel(m) {
|
|
|
488
488
|
return typeof m === "object" ? JSON.stringify(m) : String(m);
|
|
489
489
|
}
|
|
490
490
|
|
|
491
|
-
/* 极简 Markdown
|
|
491
|
+
/* 极简 Markdown 渲染(标题/加粗/行内码/列表/表格/代码块)。
|
|
492
|
+
* 围栏代码块走 codeBlockHTML(行号表格+高亮+代码主题):裸 <pre> 只有
|
|
493
|
+
* --log-bg 底色没配浅字,暖色皮肤里深底配深字根本读不了。 */
|
|
492
494
|
function md2html(md) {
|
|
493
495
|
const lines = String(md || "").split(/\r?\n/);
|
|
494
|
-
let html = [], inCode = false, inTable = false, listOpen = false;
|
|
496
|
+
let html = [], inCode = false, codeBuf = null, inTable = false, listOpen = false;
|
|
495
497
|
const inline = (t) => esc(t)
|
|
496
498
|
.replace(/`([^`]+)`/g, "<code>$1</code>")
|
|
497
499
|
.replace(/\*\*([^*]+)\*\*/g, "<b>$1</b>");
|
|
@@ -501,10 +503,11 @@ function md2html(md) {
|
|
|
501
503
|
const line = raw.replace(/\s+$/, "");
|
|
502
504
|
if (line.startsWith("```")) {
|
|
503
505
|
closeList(); closeTable();
|
|
504
|
-
html.push(
|
|
506
|
+
if (inCode) { html.push(codeBlockHTML(codeBuf.join("\n"))); codeBuf = null; }
|
|
507
|
+
else codeBuf = [];
|
|
505
508
|
inCode = !inCode; continue;
|
|
506
509
|
}
|
|
507
|
-
if (inCode) {
|
|
510
|
+
if (inCode) { codeBuf.push(raw); continue; }
|
|
508
511
|
if (/^\|(.+)\|\s*$/.test(line)) {
|
|
509
512
|
const cells = line.slice(1, -1).split("|").map((s) => s.trim());
|
|
510
513
|
if (/^[-: ]+$/.test(cells.join(""))) continue; // 分隔行
|
|
@@ -524,7 +527,7 @@ function md2html(md) {
|
|
|
524
527
|
html.push("<p>" + inline(line) + "</p>");
|
|
525
528
|
}
|
|
526
529
|
closeList(); closeTable();
|
|
527
|
-
if (inCode) html.push("
|
|
530
|
+
if (inCode && codeBuf) html.push(codeBlockHTML(codeBuf.join("\n"))); // 未闭合围栏也按代码块收尾
|
|
528
531
|
return html.join("\n");
|
|
529
532
|
}
|
|
530
533
|
|
|
@@ -564,7 +567,9 @@ function renderHealthBanner(health) {
|
|
|
564
567
|
el.className = "health-banner alerting";
|
|
565
568
|
el.title = alerts.map((p) =>
|
|
566
569
|
p.provider + (p.model ? " · " + p.model : "") +
|
|
567
|
-
|
|
570
|
+
(p.static
|
|
571
|
+
? t(":") + (p.last_error || t("未知错误"))
|
|
572
|
+
: t(":连续失败 ") + p.consecutive_failures + t(" 次(") + (p.last_error || t("未知错误")) + t(")"))
|
|
568
573
|
).join("\n");
|
|
569
574
|
if (!_healthBeeped) { _healthBeeped = true; beepAttention(); }
|
|
570
575
|
}
|
|
@@ -572,7 +577,7 @@ function renderHealthBanner(health) {
|
|
|
572
577
|
function healthBannerText(health) {
|
|
573
578
|
return ((health && health.alerts) || []).map((p) => {
|
|
574
579
|
const m = p.model ? " · " + p.model : "";
|
|
575
|
-
return "⚠ " + p.provider + m + " " + t("连接异常");
|
|
580
|
+
return "⚠ " + p.provider + m + " " + (p.static ? t("绑定链失效") : t("连接异常"));
|
|
576
581
|
}).join(t(" "));
|
|
577
582
|
}
|
|
578
583
|
|
|
@@ -581,19 +586,21 @@ function healthBannerClick() {
|
|
|
581
586
|
if (!alerts.length) return;
|
|
582
587
|
const rows = alerts.map((p) => {
|
|
583
588
|
const model = p.model ? " · " + p.model : "";
|
|
584
|
-
|
|
585
|
-
"<b>" + esc(p.provider) + (p.model ? " · " + esc(p.model) : "") + "</b>" +
|
|
589
|
+
const statLine = p.static ? "" :
|
|
586
590
|
"<div style='opacity:.75;font-size:12px;margin-top:2px'>" +
|
|
587
591
|
t("连续失败") + " " + p.consecutive_failures + " " + t("次 · 首次失败 ") + (p.first_fail_at || "-") +
|
|
588
|
-
"</div>"
|
|
592
|
+
"</div>";
|
|
593
|
+
return "<div style='margin-bottom:10px'>" +
|
|
594
|
+
"<b>" + esc(p.provider) + (p.model ? " · " + esc(p.model) : "") + "</b>" +
|
|
595
|
+
statLine +
|
|
589
596
|
"<div style='color:#dc2626;font-size:12px;margin-top:2px;word-break:break-all'>" +
|
|
590
597
|
esc(p.last_error || "") + "</div></div>";
|
|
591
598
|
}).join("");
|
|
592
599
|
const pid0 = alerts[0].provider_id || "";
|
|
593
600
|
const model0 = alerts[0].model || "";
|
|
594
601
|
const foot =
|
|
595
|
-
(model0 ? "<button class='btn ghost' onclick=\"healthDisableModel('" + esc(pid0) + "','" + esc(model0) + "')\">" + t("禁用该模型") + "</button>" : "") +
|
|
596
|
-
"<button class='btn ghost' onclick=\"healthDisableProvider('" + esc(pid0) + "')\">" + t("禁用该厂商") + "</button>" +
|
|
602
|
+
(pid0 && model0 ? "<button class='btn ghost' onclick=\"healthDisableModel('" + esc(pid0) + "','" + esc(model0) + "')\">" + t("禁用该模型") + "</button>" : "") +
|
|
603
|
+
(pid0 ? "<button class='btn ghost' onclick=\"healthDisableProvider('" + esc(pid0) + "')\">" + t("禁用该厂商") + "</button>" : "") +
|
|
597
604
|
"<button class='btn ghost' onclick=\"healthOp('silence')\">" + t("静默本次告警") + "</button>" +
|
|
598
605
|
"<button class='btn ghost' onclick=\"healthOp('reset')\">" + t("手动标记恢复") + "</button>" +
|
|
599
606
|
"<button class='primary' onclick='closeModal()'>" + t("关闭") + "</button>";
|
|
@@ -637,6 +644,7 @@ async function healthDisableModel(pid, model) {
|
|
|
637
644
|
S.provSig = "";
|
|
638
645
|
closeModal(); render(); loadOrchestrator();
|
|
639
646
|
toast(t("已禁用模型 {0} · {1},链降级自动跳过;绑定页可重新启用").replace("{0}", pid).replace("{1}", model));
|
|
647
|
+
autoRebindSoon();
|
|
640
648
|
} catch (e) {
|
|
641
649
|
toast(t("操作失败:") + e.message, true);
|
|
642
650
|
}
|
|
@@ -660,6 +668,7 @@ async function healthDisableProvider(pid) {
|
|
|
660
668
|
S.provSig = "";
|
|
661
669
|
closeModal(); render(); loadOrchestrator();
|
|
662
670
|
toast(t("已禁用厂商 {0}:链降级自动跳过,绑定页可重新启用").replace("{0}", pid));
|
|
671
|
+
autoRebindSoon();
|
|
663
672
|
} catch (e) {
|
|
664
673
|
toast(t("操作失败:") + e.message, true);
|
|
665
674
|
}
|
|
@@ -845,19 +854,19 @@ function renderBindings() {
|
|
|
845
854
|
}).join("");
|
|
846
855
|
const bound = provs.find((p) => p.id === b.provider_id);
|
|
847
856
|
const offWarn = bound && bound.enabled === false
|
|
848
|
-
? '<p class="hint warn">' + t("
|
|
857
|
+
? '<p class="hint warn">' + t("该供应商已停用:编排时不会注入它,模型链因此失效时相关步骤会直接判失败。") + '</p>' : "";
|
|
849
858
|
const protoWarn = bound && !bindable.some((p) => p.id === bound.id)
|
|
850
859
|
? '<p class="hint warn">' + t("该供应商协议为 ") + esc(protoLabel(bound)) +
|
|
851
|
-
t(",当前没有可注入的 CLI
|
|
852
|
-
//
|
|
853
|
-
//
|
|
854
|
-
//
|
|
860
|
+
t(",当前没有可注入的 CLI,编排时相关步骤会直接判失败。") + '</p>' : "";
|
|
861
|
+
// 供应商停用但链上勾选了它家模型:链条目在解析时也会被整条跳过,链空了
|
|
862
|
+
// 相关步骤会直接判失败(2026-09-17 起;此前的静默回落本机默认正是 2026-09-16
|
|
863
|
+
// 配额事故的隐藏形态)——单说「供应商下拉」警告不够,链本身的死活也要点名。
|
|
855
864
|
const chainDead = bindChain(b).some((c2) => c2.p && (() => {
|
|
856
865
|
const pv = provs.find((p) => p.id === c2.p);
|
|
857
866
|
return !pv || pv.enabled === false;
|
|
858
867
|
})());
|
|
859
868
|
const chainWarn = (!bound || bound.enabled !== false) && chainDead
|
|
860
|
-
? '<p class="hint warn">' + t("
|
|
869
|
+
? '<p class="hint warn">' + t("模型链里有已停用/已删除的供应商:这些条目解析时会被跳过,链可能因此整体失效,相关步骤将判失败。") + '</p>' : "";
|
|
861
870
|
return '<div class="card"><div class="head"><span class="name">' + esc(c.name) + "</span>" +
|
|
862
871
|
'<span class="tag">' + esc(c.orch_kind) + "</span></div>" +
|
|
863
872
|
'<div class="field"><label>' + t("供应商") + '</label><select id="bindprov-' + esc(c.id) + '">' + opts + "</select></div>" +
|
|
@@ -961,6 +970,7 @@ async function batchProvOp(op) {
|
|
|
961
970
|
try {
|
|
962
971
|
await api("/api/models/provider-op", { method: "POST",
|
|
963
972
|
body: JSON.stringify({ ids, op }) });
|
|
973
|
+
if (op !== "duplicate") autoRebindSoon();
|
|
964
974
|
} catch (e) { toast(t("操作失败:") + e.message, true); }
|
|
965
975
|
S.selProvs = {};
|
|
966
976
|
S.modelsSig = null;
|
|
@@ -1147,6 +1157,7 @@ async function keyOpCall(pid, op, keyId, key, label) {
|
|
|
1147
1157
|
try {
|
|
1148
1158
|
await api("/api/models/key-op", { method: "POST",
|
|
1149
1159
|
body: JSON.stringify({ provider_id: pid, op, key_id: keyId, key, label }) });
|
|
1160
|
+
autoRebindSoon(); // 密钥启停/删除/解冻会改变厂商「推荐可用」判定,自动补绑一次
|
|
1150
1161
|
} catch (e) { toast(t("操作失败:") + e.message, true); }
|
|
1151
1162
|
// 操作期间轮询照跑(uiPrompt 弹框不阻塞轮询),S.modelsSig 还是旧值:必须
|
|
1152
1163
|
// 清签名强制重绘,否则 KEY 写盘成功界面也看不到。
|
|
@@ -1263,6 +1274,10 @@ function provModelRow(pid, m, group) {
|
|
|
1263
1274
|
esc((tm.error || "").slice(0, 24)) + "</span>") : "";
|
|
1264
1275
|
const price = (m.price_in != null && m.price_out != null)
|
|
1265
1276
|
? '<span class="hint">¥' + esc(m.price_in) + "/¥" + esc(m.price_out) + "</span>" : "";
|
|
1277
|
+
const imgcap = '<span class="tag imgcap' + (m.image_in ? " ok" : "") + '" title="' +
|
|
1278
|
+
(m.image_in ? t("支持图片输入,点击关闭")
|
|
1279
|
+
: t("纯文本模型,点击开启图片输入(内置智能体传图以此为准)")) + '"' +
|
|
1280
|
+
' onclick="toggleModelImage(\'' + esc(pid) + '\', \'' + esc(m.name) + '\')">' + t("图") + "</span>";
|
|
1266
1281
|
const sel = modelSel(pid);
|
|
1267
1282
|
return '<div class="prow' + (m.enabled ? "" : " off") + '" draggable="true" data-group="' +
|
|
1268
1283
|
esc(group) + '" data-name="' + esc(m.name) + '">' +
|
|
@@ -1272,7 +1287,7 @@ function provModelRow(pid, m, group) {
|
|
|
1272
1287
|
'<span class="drag" title="' + t("拖动调整优先级") + '"><svg class="ico" aria-hidden="true"><use href="#i-grip"></use></svg></span>' +
|
|
1273
1288
|
'<span class="pprio">#' + m.priority + "</span>" +
|
|
1274
1289
|
'<span class="pname" title="' + esc(m.name) + '">' + esc(m.name) + "</span>" +
|
|
1275
|
-
price + tmHtml +
|
|
1290
|
+
price + imgcap + tmHtml +
|
|
1276
1291
|
'<span class="row-ops">' +
|
|
1277
1292
|
'<button class="ghost small row-op" onclick="testModelBtn(\'' + esc(pid) + '\', \'' + esc(m.name) + '\')">' + t("测试") + '</button>' +
|
|
1278
1293
|
'<button class="danger small row-op" title="' + t("从列表删除:刷新/重新导入不会再带回,可在分组底部恢复") + '"' +
|
|
@@ -1352,6 +1367,7 @@ async function modelOp(pid, name, op) {
|
|
|
1352
1367
|
try {
|
|
1353
1368
|
await api("/api/models/model-op", { method: "POST",
|
|
1354
1369
|
body: JSON.stringify({ provider_id: pid, name, op }) });
|
|
1370
|
+
autoRebindSoon(); // 模型停用/启用/删除会改变推荐源与死链判定,自动补绑一次
|
|
1355
1371
|
const s = modelSel(pid); // 单行操作后同步清掉该行勾选
|
|
1356
1372
|
if (s[name]) { delete modelSelSet(pid)[name]; }
|
|
1357
1373
|
} catch (e) { toast(t("操作失败:") + e.message, true); }
|
|
@@ -1359,6 +1375,21 @@ async function modelOp(pid, name, op) {
|
|
|
1359
1375
|
poll();
|
|
1360
1376
|
}
|
|
1361
1377
|
|
|
1378
|
+
function toggleModelImage(pid, name) {
|
|
1379
|
+
const p = (S.providers || []).find((x) => x.id === pid);
|
|
1380
|
+
const m = p && (p.models || []).find((x) => x.name === name);
|
|
1381
|
+
api("/api/models/model-caps", { method: "POST",
|
|
1382
|
+
body: JSON.stringify({ provider_id: pid, name, image_in: !(m && m.image_in) }) })
|
|
1383
|
+
.then(() => { S.modelsSig = null; poll(); })
|
|
1384
|
+
.catch((e) => toast(t("保存失败:") + e.message, true));
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
/* 反查模型是否声明图片输入(绑定页 chip/列表只读徽标用) */
|
|
1388
|
+
function modelHasImage(pid, name) {
|
|
1389
|
+
const p = (S.providers || []).find((x) => x.id === pid);
|
|
1390
|
+
return !!(p && (p.models || []).some((x) => x.name === name && x.image_in));
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1362
1393
|
/* ---- 模型批量选择 ---- */
|
|
1363
1394
|
function toggleModelSel(pid, name, on) {
|
|
1364
1395
|
const s = modelSelSet(pid);
|
|
@@ -1403,6 +1434,7 @@ async function batchModelOp(pid, op) {
|
|
|
1403
1434
|
try {
|
|
1404
1435
|
await api("/api/models/model-op", { method: "POST",
|
|
1405
1436
|
body: JSON.stringify({ provider_id: pid, names, op }) });
|
|
1437
|
+
autoRebindSoon();
|
|
1406
1438
|
} catch (e) { toast(t("操作失败:") + e.message, true); }
|
|
1407
1439
|
clearModelSel(pid);
|
|
1408
1440
|
poll();
|
|
@@ -1421,6 +1453,7 @@ async function toggleProviderEnabled(pid, enabled) {
|
|
|
1421
1453
|
try {
|
|
1422
1454
|
await api("/api/models/provider-op", { method: "POST",
|
|
1423
1455
|
body: JSON.stringify({ ids: [pid], op: off ? "enable" : "disable" }) });
|
|
1456
|
+
autoRebindSoon();
|
|
1424
1457
|
} catch (e) { toast(t("操作失败:") + e.message, true); }
|
|
1425
1458
|
S.modelsSig = null;
|
|
1426
1459
|
poll();
|
|
@@ -1444,7 +1477,8 @@ async function refreshAllModels() {
|
|
|
1444
1477
|
btn.disabled = false; btn.classList.remove("loading");
|
|
1445
1478
|
btn.title = t("全部重新拉取模型列表");
|
|
1446
1479
|
}, 1500);
|
|
1447
|
-
setTimeout(poll, 2500);
|
|
1480
|
+
setTimeout(poll, 2500);
|
|
1481
|
+
setTimeout(autoRebindSoon, 8000); // 模型列表落盘后推荐源才完整,补绑一次
|
|
1448
1482
|
}
|
|
1449
1483
|
|
|
1450
1484
|
async function refreshProviderModels(id) {
|
|
@@ -1455,7 +1489,7 @@ async function refreshProviderModels(id) {
|
|
|
1455
1489
|
else toast(t("已获取模型列表 · wire 适配测试后台进行中"));
|
|
1456
1490
|
} catch (e) { toast(t("获取失败:") + e.message, true); }
|
|
1457
1491
|
poll();
|
|
1458
|
-
setTimeout(
|
|
1492
|
+
setTimeout(autoRebindSoon, 4000); // 模型列表落盘后补绑一次(后台 wire 探测同步进行)
|
|
1459
1493
|
}
|
|
1460
1494
|
|
|
1461
1495
|
/* 供应商编辑卡:协议可选「自动」;显式选定时它是主协议(用于需要唯一协议的
|
|
@@ -1511,6 +1545,7 @@ async function delProvider(id) {
|
|
|
1511
1545
|
try {
|
|
1512
1546
|
await api("/api/models/provider-op", { method: "POST",
|
|
1513
1547
|
body: JSON.stringify({ ids: [id], op: "delete" }) });
|
|
1548
|
+
autoRebindSoon();
|
|
1514
1549
|
} catch (e) { toast(t("操作失败:") + e.message, true); }
|
|
1515
1550
|
if (S.selModels) delete S.selModels[id];
|
|
1516
1551
|
if (S.selProvs) delete S.selProvs[id];
|
|
@@ -1574,6 +1609,7 @@ async function doAddProvider() {
|
|
|
1574
1609
|
S.modelsSig = null;
|
|
1575
1610
|
closeModal();
|
|
1576
1611
|
poll();
|
|
1612
|
+
autoRebindSoon(); // 新增即启用:马上给空链/死链一次推荐机会
|
|
1577
1613
|
if (p && body.api_key) refreshProviderModels(p.id); // 有密钥才自动拉模型列表
|
|
1578
1614
|
} catch (e) {
|
|
1579
1615
|
res.textContent = t("保存失败:") + e.message;
|
|
@@ -1669,7 +1705,7 @@ async function doImport() {
|
|
|
1669
1705
|
btn.textContent = t("完成");
|
|
1670
1706
|
btn.disabled = false;
|
|
1671
1707
|
btn.onclick = closeModal;
|
|
1672
|
-
if (r.imported) { S.selProv = null; S.modelsSig = null; poll(); }
|
|
1708
|
+
if (r.imported) { S.selProv = null; S.modelsSig = null; poll(); autoRebindSoon(); }
|
|
1673
1709
|
} catch (e) {
|
|
1674
1710
|
$("import-result").innerHTML = '<div class="msg bad">' + t("导入失败:") + esc(e.message) + "</div>";
|
|
1675
1711
|
btn.disabled = false; btn.textContent = t("导入选中");
|
|
@@ -1786,6 +1822,9 @@ async function createTask() {
|
|
|
1786
1822
|
try {
|
|
1787
1823
|
const r = await api("/api/tasks", { method: "POST", body: JSON.stringify(payload) });
|
|
1788
1824
|
msg.textContent = t("已创建,跳转运行页…");
|
|
1825
|
+
// 先把新任务刷进 state 再跳:chatEngineIsDirect 靠 S.state.tasks 判引擎,
|
|
1826
|
+
// 不刷的话对话页签不会就绪,自动选卡落不到「对话」
|
|
1827
|
+
try { await refreshState(); } catch (e) { /* 刷失败等轮询兜底 */ }
|
|
1789
1828
|
jumpToRun(r.run_id);
|
|
1790
1829
|
$("f-goal").value = "";
|
|
1791
1830
|
S.atts = []; renderAttachChips(); // 附件已移交任务待提交区,清空本地列表
|
|
@@ -2083,7 +2122,7 @@ function bindCtxMenus() {
|
|
|
2083
2122
|
items.push({ label: t("复制工作目录路径"), fn: () => revealPath("tasks", taskId, false) });
|
|
2084
2123
|
if (runId) items.push({ label: t("复制日志目录路径"), fn: () => revealPath("runs", runId, false) });
|
|
2085
2124
|
const st = det.dataset.status || "";
|
|
2086
|
-
if (st === "failed" || st === "cancelled") items.push({ label: t("↻
|
|
2125
|
+
if (st === "failed" || st === "cancelled") items.push({ label: t("↻ 继续任务"), fn: () => retryTask(taskId) });
|
|
2087
2126
|
// 与详情页同一套说法:失败/取消叫「编辑重试」,其余叫「基于此任务新建」
|
|
2088
2127
|
items.push({ label: (st === "failed" || st === "cancelled") ? t("✎ 编辑重试") : t("基于此任务新建"),
|
|
2089
2128
|
fn: () => newFromTask(taskId) });
|
|
@@ -3093,6 +3132,7 @@ function closeRun() {
|
|
|
3093
3132
|
stopHiveTick();
|
|
3094
3133
|
detailSideReset();
|
|
3095
3134
|
$("run-detail").classList.add("hidden");
|
|
3135
|
+
renderChatNav(); // 解除 main.chat-fill(对话为主的固定高度),恢复外层滚动
|
|
3096
3136
|
if (document.body.classList.contains("settings-mode")) {
|
|
3097
3137
|
document.querySelector("#sub-runs .panel:first-child").classList.remove("hidden");
|
|
3098
3138
|
} else {
|
|
@@ -3152,7 +3192,55 @@ function applyRdTabs() {
|
|
|
3152
3192
|
});
|
|
3153
3193
|
document.querySelectorAll("#run-detail .rd-pane").forEach((p) =>
|
|
3154
3194
|
p.classList.toggle("hidden", p.dataset.pane !== S.rdTab));
|
|
3155
|
-
|
|
3195
|
+
// 对话页签不放日志抽屉:会盖住贴底输入条(手动切来/自动选卡都覆盖)
|
|
3196
|
+
if (S.rdTab === "chat" && !$("rd-log").classList.contains("hidden")) window.rdLogClose();
|
|
3197
|
+
renderChatNav();
|
|
3198
|
+
}
|
|
3199
|
+
|
|
3200
|
+
/* 直连任务「对话为主」布局:收起常规页签条,右上角一排小胶囊按需打开
|
|
3201
|
+
* 蜂巢/步骤/成果等分区;离开对话时给「返回对话」入口(用户 2026-09-17 拍板:
|
|
3202
|
+
* 对话场景其它页签用处不大,默认关掉、需要再点开)。 */
|
|
3203
|
+
function renderChatNav() {
|
|
3204
|
+
const nav = $("rd-chat-nav"), detail = $("run-detail");
|
|
3205
|
+
if (!nav || !detail) return;
|
|
3206
|
+
const direct = !detail.classList.contains("hidden") && !!S.lastRun &&
|
|
3207
|
+
chatEngineIsDirect(S.lastRun);
|
|
3208
|
+
detail.classList.toggle("chat-mode", direct);
|
|
3209
|
+
// 对话为主时锁死外层滚动(main 是滚动根);离开详情必须解锚,否则任务列表滚不动
|
|
3210
|
+
const mainEl = document.querySelector("main");
|
|
3211
|
+
if (mainEl) mainEl.classList.toggle("chat-fill", direct);
|
|
3212
|
+
if (!direct) { nav.classList.add("hidden"); nav.innerHTML = ""; return; }
|
|
3213
|
+
const labels = { hive: t("蜂巢"), steps: t("步骤"), result: t("成果"),
|
|
3214
|
+
git: "Git", bible: t("圣经"), bookmeta: t("作品信息") };
|
|
3215
|
+
const avail = rdTabAvail();
|
|
3216
|
+
let pills = "";
|
|
3217
|
+
for (const k of ["hive", "steps", "result", "git", "bible", "bookmeta"]) {
|
|
3218
|
+
if (!avail[k]) continue;
|
|
3219
|
+
// 徽章直接镜像隐藏页签上的(蜂巢在岗数/步骤数/待裁决/成果数),不另设状态源
|
|
3220
|
+
const badge = document.querySelector('#rd-tabs .rd-tab[data-tab="' + k + '"] .rd-badge');
|
|
3221
|
+
pills += '<button class="cn-pill' + (S.rdTab === k ? " on" : "") +
|
|
3222
|
+
'" onclick="rdChatNavGo(\'' + k + '\')">' + labels[k] +
|
|
3223
|
+
(badge && badge.textContent ? "<b>" + esc(badge.textContent) + "</b>" : "") + "</button>";
|
|
3224
|
+
}
|
|
3225
|
+
nav.innerHTML = (S.rdTab !== "chat"
|
|
3226
|
+
? '<button class="cn-back" onclick="rdChatNavBack()">' +
|
|
3227
|
+
'<svg class="ico" aria-hidden="true"><use href="#i-arrow-left"></use></svg>' +
|
|
3228
|
+
t("返回对话") + "</button>"
|
|
3229
|
+
: "") +
|
|
3230
|
+
'<span class="cn-pills">' + pills + "</span>";
|
|
3231
|
+
nav.classList.remove("hidden");
|
|
3232
|
+
}
|
|
3233
|
+
window.rdChatNavGo = function (tab) {
|
|
3234
|
+
S.rdTab = tab;
|
|
3235
|
+
S.rdTabPin = true; // 用户主动去看其它分区:别被自动选卡拽回对话
|
|
3236
|
+
applyRdTabs();
|
|
3237
|
+
};
|
|
3238
|
+
window.rdChatNavBack = function () {
|
|
3239
|
+
S.rdTab = "chat";
|
|
3240
|
+
S.rdTabPin = false;
|
|
3241
|
+
S.rdTabSig = "";
|
|
3242
|
+
applyRdTabs();
|
|
3243
|
+
};
|
|
3156
3244
|
|
|
3157
3245
|
/* 徽章:蜂巢=在岗数(运行中)、步骤=总步数、版本=待裁决、成果=文件数(loadArtifacts 里刷) */
|
|
3158
3246
|
function rdTabBadges() {
|
|
@@ -3173,8 +3261,11 @@ function rdTabBadges() {
|
|
|
3173
3261
|
function rdTabsSync(ctx) {
|
|
3174
3262
|
if (ctx) S._rdCtx = ctx;
|
|
3175
3263
|
if (ctx && !S.rdTabPin) {
|
|
3264
|
+
// 对话页签可用性入签名:刚建的任务要等 state 刷进来 direct 才判定成立,
|
|
3265
|
+
// chat 从不可用变可用时必须触发一次重新选卡(否则落在步骤/蜂巢不跳对话)
|
|
3266
|
+
const chatAvail = $("rd-chat") && !$("rd-chat").classList.contains("hidden");
|
|
3176
3267
|
const sig = (S.detailTaskKey || S.detailRunId || "") + "|" + (ctx.status || "") +
|
|
3177
|
-
"|" + (ctx.gitState || "") + "|" + (ctx.running ? 1 : 0);
|
|
3268
|
+
"|" + (ctx.gitState || "") + "|" + (ctx.running ? 1 : 0) + "|" + (chatAvail ? 1 : 0);
|
|
3178
3269
|
if (S.rdTabSig !== sig) {
|
|
3179
3270
|
S.rdTabSig = sig;
|
|
3180
3271
|
const avail = rdTabAvail();
|
|
@@ -3370,11 +3461,13 @@ const BOOKMETA_PLATFORMS = [
|
|
|
3370
3461
|
];
|
|
3371
3462
|
const BOOKMETA_FIELDS = {
|
|
3372
3463
|
fanqie: [["book_name", "作品名"], ["signing_mode", "签约模式"], ["target_reader", "目标读者"],
|
|
3373
|
-
["category", "
|
|
3374
|
-
["tags_plot", "情节标签"], ["
|
|
3375
|
-
["
|
|
3464
|
+
["category", "主分类"], ["tags_theme", "主题标签"], ["tags_role", "角色标签"],
|
|
3465
|
+
["tags_plot", "情节标签"], ["content_plot", "内容·情节"], ["content_emotion", "内容·情感"],
|
|
3466
|
+
["content_character", "内容·人设"], ["content_world", "内容·世界观"],
|
|
3467
|
+
["protagonist_1", "主角名1"], ["protagonist_2", "主角名2"], ["summary", "作品简介"]],
|
|
3376
3468
|
qimao: [["book_name", "作品名称"], ["target_reader", "目标读者"], ["category_main", "一级分类"],
|
|
3377
|
-
["category_sub", "二级分类"], ["
|
|
3469
|
+
["category_sub", "二级分类"], ["tags_style", "风格标签"], ["tags_role", "角色标签"],
|
|
3470
|
+
["tags_plot", "情节标签"], ["tags_bg", "背景标签"], ["protagonist_1", "主角名1"],
|
|
3378
3471
|
["protagonist_2", "主角名2"], ["status", "作品状态"], ["summary", "作品简介"]],
|
|
3379
3472
|
};
|
|
3380
3473
|
|
|
@@ -3486,6 +3579,7 @@ window.bmGen = async function (taskId, platform) {
|
|
|
3486
3579
|
await api("/api/tasks/" + encodeURIComponent(taskId) + "/book-meta",
|
|
3487
3580
|
{ method: "POST", body: JSON.stringify({ platform }) });
|
|
3488
3581
|
toast(t("已开始生成,完成后这里会自动更新"));
|
|
3582
|
+
await refreshState(); render(); // 立即翻到「生成中」,不等 SSE 推送/下一轮轮询
|
|
3489
3583
|
} catch (e) { toast(t("生成失败:") + e.message, true); }
|
|
3490
3584
|
};
|
|
3491
3585
|
|
|
@@ -3604,7 +3698,7 @@ function _renderGitSnapshot(run, task) {
|
|
|
3604
3698
|
'<span class="chip failed">' + t("检出失败") + "</span>" +
|
|
3605
3699
|
'<code class="git-branch">codebee/' + esc(tid || t("(无主运行)")) + t("(") + esc(t("未创建")) + t(")") + "</code></div>" +
|
|
3606
3700
|
(lastErr ? '<div class="hint warn">' + esc(lastErr) + "</div>" : "") +
|
|
3607
|
-
'<div class="hint">' + esc(t("
|
|
3701
|
+
'<div class="hint">' + esc(t("处理后点「继续任务」,运行会重新检出任务分支。")) + "</div>";
|
|
3608
3702
|
rdTabsSync();
|
|
3609
3703
|
return;
|
|
3610
3704
|
}
|
|
@@ -4021,7 +4115,7 @@ function drawInspector() {
|
|
|
4021
4115
|
main.innerHTML = '<div class="insp-branch"><code>codebee/' + esc(d.task.id || "") +
|
|
4022
4116
|
t("(") + esc(t("未创建")) + t(")") + '</code></div>' +
|
|
4023
4117
|
(lastErr ? '<div class="hint warn">' + esc(lastErr) + "</div>" : "") +
|
|
4024
|
-
'<span class="insp-hint">' + esc(t("
|
|
4118
|
+
'<span class="insp-hint">' + esc(t("处理后点「继续任务」,运行会重新检出任务分支。")) + "</span>";
|
|
4025
4119
|
} else {
|
|
4026
4120
|
// 没指定代码版本:没有任务分支,卡里只留一句说明
|
|
4027
4121
|
chipEl.classList.add("hidden");
|
|
@@ -4650,7 +4744,8 @@ const hiveTails = {}; // "runid|rel" -> 最近一行输出缓
|
|
|
4650
4744
|
function stopHiveTick() { if (hiveTimer) { clearInterval(hiveTimer); hiveTimer = null; } }
|
|
4651
4745
|
|
|
4652
4746
|
/* run.steps -> 阶段泳道流水线:规划→起草→评审→修订→打磨→合成,先后关系一眼可见;
|
|
4653
|
-
*
|
|
4747
|
+
* 运行中蜜蜂摆动+秒表走动+蜜光呼吸;彗尾光点流向下一阶段;点格即看实时输出;
|
|
4748
|
+
* 入场瀑布只在步骤结构真变时重播(签名闸门,轮询空转不重建 DOM)。 */
|
|
4654
4749
|
function hiveStage(role) {
|
|
4655
4750
|
const r = String(role || "");
|
|
4656
4751
|
if (/^(plan|outline)/.test(r)) return t("规划");
|
|
@@ -4711,7 +4806,7 @@ window.renderHive = function (run) {
|
|
|
4711
4806
|
if (sub) sub.textContent = running.length
|
|
4712
4807
|
? t("在岗") + " " + running.length + " / " + steps.length : t("全部空闲");
|
|
4713
4808
|
const activeIdx = lanes.map((l) => byStage[l].some((s) => s.status === "running")).lastIndexOf(true);
|
|
4714
|
-
const cell = (s) => {
|
|
4809
|
+
const cell = (s, laneIdx, cellIdx) => {
|
|
4715
4810
|
// 五归一:取消/超时的格子不再冒充「完成」——与步骤芯片同三色体系
|
|
4716
4811
|
const st = s.status === "running" || s.status === "queued" ? "running"
|
|
4717
4812
|
: s.status === "failed" ? "failed"
|
|
@@ -4724,30 +4819,37 @@ window.renderHive = function (run) {
|
|
|
4724
4819
|
const key = run.id + "|" + (s.log || "");
|
|
4725
4820
|
if (st !== "running") delete hiveTails[key];
|
|
4726
4821
|
const tailText = st === "running" ? (hiveTails[key] || concl) : concl;
|
|
4727
|
-
const title = [s.role, who,
|
|
4822
|
+
const title = [s.role, who,
|
|
4823
|
+
(s.duration_s != null ? s.duration_s + "s" : ""),
|
|
4824
|
+
s.started_at ? t("开始于 ") + s.started_at : "",
|
|
4728
4825
|
(s.note ? "◆ " + s.note : "")].filter(Boolean).join(" · ")
|
|
4729
4826
|
+ (concl && st !== "running" ? "\n" + t("结论:") + concl : "");
|
|
4730
|
-
|
|
4827
|
+
// --d:入场瀑布逐格延迟(泳道间 110ms + 同泳道逐格 45ms),样式端消费
|
|
4828
|
+
return '<div class="hive-cell st-' + st + (st === "running" ? " hc-breathe" : "") + '"' +
|
|
4829
|
+
' style="--d:' + (laneIdx * 110 + cellIdx * 45) + 'ms"' +
|
|
4830
|
+
' title="' + esc(title) + '" ' +
|
|
4731
4831
|
'onclick="hiveOpenLog(\'' + esc(run.id) + "', '" + esc(s.log || "") + '\')">' +
|
|
4732
4832
|
'<div class="hc-head">' +
|
|
4733
|
-
'<
|
|
4833
|
+
'<img class="hc-bee" src="icons/bee.svg" alt="" aria-hidden="true">' +
|
|
4734
4834
|
'<span class="hc-role">' + esc(s.role || "") + "</span>" +
|
|
4735
4835
|
'<span class="hc-who">' + esc(who) + "</span></div>" +
|
|
4736
4836
|
'<div class="hc-tail" data-log="' + esc(s.log || "") + '">' +
|
|
4737
4837
|
esc(tailText) + "</div>" +
|
|
4738
4838
|
'<div class="hc-meta"><span class="hc-elapsed" data-started="' + esc(s.started_at || "") + '">' +
|
|
4739
4839
|
(s.duration_s != null ? s.duration_s + "s" : hiveElapsed(s.started_at)) + "</span>" +
|
|
4740
|
-
(st === "running" ? '<span class="hc-live"
|
|
4840
|
+
(st === "running" ? '<span class="hc-live"><i class="live-dot"></i>' + t("工作中") + "</span>" : "") +
|
|
4741
4841
|
(st === "timeout" ? '<span class="hc-dead">⏱ ' + t("超时") + "</span>" : "") +
|
|
4742
4842
|
(st === "cancelled" ? '<span class="hc-dead">' + t("已取消") + "</span>" : "") +
|
|
4743
4843
|
"</div></div>";
|
|
4744
4844
|
};
|
|
4745
|
-
const dots = (list) => list.slice(-12).map((s) => {
|
|
4845
|
+
const dots = (list, laneIdx) => list.slice(-12).map((s, di) => {
|
|
4746
4846
|
const dc = s.status === "running" ? "run" : s.status === "failed" ? "fail"
|
|
4747
4847
|
: s.status === "cancelled" ? "cancel" : s.status === "timeout" ? "time" : "done";
|
|
4748
4848
|
const stx = { running: t("运行中"), failed: t("失败"), cancelled: t("已取消"),
|
|
4749
4849
|
timeout: t("超时"), done: t("完成") }[dc] || s.status;
|
|
4750
|
-
|
|
4850
|
+
// --d:与格子同款级联延迟——入场时整条轨道按执行顺序"哗"地点亮(进度回放感)
|
|
4851
|
+
return '<i class="ld ld-' + dc + '" style="--d:' + (laneIdx * 110 + di * 45) + 'ms"' +
|
|
4852
|
+
' title="' + esc((s.role || "") + " · " + stx) + '"></i>';
|
|
4751
4853
|
}).join("");
|
|
4752
4854
|
$("rd-hive-cells").innerHTML = lanes.map((stage, i) => {
|
|
4753
4855
|
const list = byStage[stage];
|
|
@@ -4755,13 +4857,44 @@ window.renderHive = function (run) {
|
|
|
4755
4857
|
// 无 running(终态回看):全部视为已完成泳道;有 running 时 activeIdx 之前算完成
|
|
4756
4858
|
const cls = hasRun ? "lane-active"
|
|
4757
4859
|
: (activeIdx === -1 || i < activeIdx ? "lane-done" : "lane-idle");
|
|
4860
|
+
// 泳道头视觉 v2:阶段序号徽章(01/02…流水线站序)+ 已完成进度(settled/total)
|
|
4861
|
+
const settled = list.filter((s) => !["running", "queued"].includes(s.status)).length;
|
|
4758
4862
|
return '<div class="hive-lane ' + cls + '">' +
|
|
4759
|
-
'<div class="lane-head"
|
|
4863
|
+
'<div class="lane-head">' +
|
|
4864
|
+
'<span class="lane-idx">' + String(i + 1).padStart(2, "0") + "</span>" +
|
|
4865
|
+
'<span class="lane-name">' + esc(stage) + '</span>' +
|
|
4760
4866
|
'<span class="lane-n">×' + list.length + "</span>" +
|
|
4761
|
-
(
|
|
4762
|
-
'<
|
|
4763
|
-
'<div class="lane-
|
|
4764
|
-
|
|
4867
|
+
(settled ? '<span class="lane-prog">' + settled + "/" + list.length + "</span>" : "") +
|
|
4868
|
+
(hasRun ? '<span class="lane-live"><i class="live-dot"></i>' + t("进行中") + "</span>" : "") + "</div>" +
|
|
4869
|
+
'<div class="lane-track">' + dots(list, i) + "</div>" +
|
|
4870
|
+
'<div class="lane-cells">' + list.slice(-8).map((s, ci) => cell(s, i, ci)).join("") + "</div></div>";
|
|
4871
|
+
}).join('<div class="lane-flow" aria-hidden="true">'
|
|
4872
|
+
+ '<svg class="flow-line flow-line-down" viewBox="0 0 36 44" aria-hidden="true">'
|
|
4873
|
+
+ '<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>'
|
|
4874
|
+
+ '<svg class="flow-line flow-line-up" viewBox="0 0 36 44" aria-hidden="true">'
|
|
4875
|
+
+ '<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>'
|
|
4876
|
+
+ '<span class="flow-bee"><img src="icons/bee.svg" alt="" aria-hidden="true"></span>'
|
|
4877
|
+
+ '</div>');
|
|
4878
|
+
// 入场瀑布闸门:只按"结构"(泳道数/格数/状态构成)签名;运行中尾巴/秒表每 2s
|
|
4879
|
+
// 变化不能触重播。签名没变就不动 DOM——格子不闪、动画不重启、悬停不弹手。
|
|
4880
|
+
const sig = lanes.map((l) => l + ":" + byStage[l].length + "(" +
|
|
4881
|
+
byStage[l].map((s) => s.status[0] || "?").join("") + ")").join("|");
|
|
4882
|
+
const cellsBox = $("rd-hive-cells");
|
|
4883
|
+
if (cellsBox.dataset.hiveSig !== sig) {
|
|
4884
|
+
const fresh = cellsBox.dataset.hiveSig == null; // 首次展开详情也照播,先落框架再逐格亮起
|
|
4885
|
+
cellsBox.dataset.hiveSig = sig;
|
|
4886
|
+
// hive-enter 只在下一帧就摘(Rune/Hermes 式一闪而过的瀑布),全程保留会锁死
|
|
4887
|
+
// 入场帧;1400ms 定时是 RAF 被后台标签页冻结时的兜底。
|
|
4888
|
+
if (!fresh) cellsBox.classList.remove("hive-enter");
|
|
4889
|
+
cellsBox.classList.add("hive-enter");
|
|
4890
|
+
if (cellsBox._hiveEnterRAF) cancelAnimationFrame(cellsBox._hiveEnterRAF);
|
|
4891
|
+
cellsBox._hiveEnterRAF = requestAnimationFrame(() => {
|
|
4892
|
+
requestAnimationFrame(() => cellsBox.classList.remove("hive-enter"));
|
|
4893
|
+
cellsBox._hiveEnterRAF = null;
|
|
4894
|
+
});
|
|
4895
|
+
if (cellsBox._hiveEnterT) clearTimeout(cellsBox._hiveEnterT);
|
|
4896
|
+
cellsBox._hiveEnterT = setTimeout(() => cellsBox.classList.remove("hive-enter"), 1400);
|
|
4897
|
+
}
|
|
4765
4898
|
const live = running.length && (run.status === "running" || run.status === "queued");
|
|
4766
4899
|
if (hiveClock) { clearInterval(hiveClock); hiveClock = null; }
|
|
4767
4900
|
if (live) {
|
|
@@ -4773,12 +4906,8 @@ window.renderHive = function (run) {
|
|
|
4773
4906
|
el.textContent = hiveElapsed(el.dataset.started);
|
|
4774
4907
|
});
|
|
4775
4908
|
}, 1000);
|
|
4776
|
-
//
|
|
4777
|
-
//
|
|
4778
|
-
if (S.hiveAutoLog !== run.id && !currentLog) {
|
|
4779
|
-
const first = running.find((s) => s.log);
|
|
4780
|
-
if (first) { S.hiveAutoLog = run.id; toggleLog(run.id, first.log); }
|
|
4781
|
-
}
|
|
4909
|
+
// 日志抽屉一律手动打开(点蜂巢格/步骤行):自动弹开会盖住对话输入条,
|
|
4910
|
+
// 用户要求所有页面默认不展示日志(2026-09-17)
|
|
4782
4911
|
} else stopHiveTick();
|
|
4783
4912
|
rdTabsSync(); // 蜂巢显隐直接决定「蜂巢」标签可用性
|
|
4784
4913
|
};
|
|
@@ -4993,6 +5122,38 @@ function chatEngineIsDirect(run) {
|
|
|
4993
5122
|
return !!(t && t.engine === "direct");
|
|
4994
5123
|
}
|
|
4995
5124
|
|
|
5125
|
+
/* 对话正文:``` 围栏渲染成真代码块(复用 codeBlockHTML:行号+高亮+代码主题),
|
|
5126
|
+
* 其余文本保持 pre-wrap 原样(行内 `code` 与 **加粗** 轻量翻译)。模型输出先
|
|
5127
|
+
* 全量 esc 再插标记;未闭合围栏按代码块收尾,流式输出中途也不撒裸反引号。 */
|
|
5128
|
+
function chatBodyHTML(text) {
|
|
5129
|
+
const lines = String(text || "").split(/\r?\n/);
|
|
5130
|
+
const out = [];
|
|
5131
|
+
let txt = [], code = null;
|
|
5132
|
+
const flushTxt = () => {
|
|
5133
|
+
if (!txt.length) return;
|
|
5134
|
+
out.push('<div class="chat-body">' + txt.map((l) => {
|
|
5135
|
+
let h = esc(l);
|
|
5136
|
+
h = h.replace(/`([^`]+)`/g, "<code>$1</code>");
|
|
5137
|
+
h = h.replace(/\*\*([^*]+)\*\*/g, "<b>$1</b>");
|
|
5138
|
+
return h;
|
|
5139
|
+
}).join("\n") + "</div>");
|
|
5140
|
+
txt = [];
|
|
5141
|
+
};
|
|
5142
|
+
for (const raw of lines) {
|
|
5143
|
+
const line = raw.replace(/\s+$/, "");
|
|
5144
|
+
if (line.startsWith("```")) {
|
|
5145
|
+
if (code) { out.push(codeBlockHTML(code.join("\n"))); code = null; }
|
|
5146
|
+
else { flushTxt(); code = []; }
|
|
5147
|
+
continue;
|
|
5148
|
+
}
|
|
5149
|
+
if (code) code.push(raw);
|
|
5150
|
+
else txt.push(raw);
|
|
5151
|
+
}
|
|
5152
|
+
if (code) out.push(codeBlockHTML(code.join("\n")));
|
|
5153
|
+
flushTxt();
|
|
5154
|
+
return out.join("");
|
|
5155
|
+
}
|
|
5156
|
+
|
|
4996
5157
|
async function renderChat(run, active) {
|
|
4997
5158
|
const box = $("rd-chat");
|
|
4998
5159
|
if (!box) return;
|
|
@@ -5013,36 +5174,59 @@ async function renderChat(run, active) {
|
|
|
5013
5174
|
// 指向当前详情正在展示的那条 run——拉取期间切了详情就丢弃响应
|
|
5014
5175
|
if (!S.lastRun || S.lastRun.id !== runForFetch) return;
|
|
5015
5176
|
const flow = $("rd-chat-flow");
|
|
5177
|
+
// 时间显示统一收敛到 HH:MM(日期在 meta 条里,全量戳塞气泡就是噪音);
|
|
5178
|
+
// 附件兼容 字符串路径 / {name|path} 对象 两种形态(对象直接拼会成 [object Object])
|
|
5179
|
+
const chatTime = (v) => { const s = String(v || "");
|
|
5180
|
+
return /^\d{4}-\d{2}-\d{2}[ T]/.test(s) ? s.slice(11, 16) : s; };
|
|
5181
|
+
const attName = (a) => { const p = typeof a === "string" ? a
|
|
5182
|
+
: ((a && (a.name || a.path)) || "");
|
|
5183
|
+
return String(p).split(/[\\/]/).pop() || ""; };
|
|
5016
5184
|
flow.innerHTML = items.map((it) => {
|
|
5017
5185
|
if (it.kind === "user") {
|
|
5018
|
-
//
|
|
5186
|
+
// 用户消息:右侧浅灰圆角块,元信息在块内顶部。
|
|
5187
|
+
// 附件胶囊可点开预览:data-att 带 _attachments/ 相对路径(老数据裸文件名
|
|
5188
|
+
// 在前端钉回 _attachments/,与服务端 norm_rel 同口径),事件委托绑在
|
|
5189
|
+
// rd-chat-flow 上——时间线整块 innerHTML 重建也不丢监听
|
|
5190
|
+
const atts = (it.attachments || []).map((a) => {
|
|
5191
|
+
const nm = attName(a);
|
|
5192
|
+
if (!nm) return null;
|
|
5193
|
+
let rel = typeof a === "string" ? a : ((a && (a.path || a.name)) || nm);
|
|
5194
|
+
rel = String(rel).replace(/\\/g, "/");
|
|
5195
|
+
if (!rel.startsWith("_attachments/")) rel = "_attachments/" + rel.split("/").pop();
|
|
5196
|
+
return { nm, rel };
|
|
5197
|
+
}).filter(Boolean);
|
|
5019
5198
|
return '<div class="chat-row me">' +
|
|
5020
5199
|
'<div class="chat-bubble me">' +
|
|
5021
|
-
'<div class="chat-meta">' + esc(it.who || "") + " " + esc(it.at
|
|
5200
|
+
'<div class="chat-meta">' + esc(it.who || "") + " " + esc(chatTime(it.at)) +
|
|
5022
5201
|
(it.consumed ? "" : " · " + t("待送达")) + "</div>" +
|
|
5023
5202
|
esc(it.text || t("(仅附件)")) +
|
|
5024
|
-
(
|
|
5025
|
-
? '<div class="chat-atts">' +
|
|
5026
|
-
'<span class="att-chip2"
|
|
5203
|
+
(atts.length
|
|
5204
|
+
? '<div class="chat-atts">' + atts.map((a) =>
|
|
5205
|
+
'<span class="att-chip2 att-open" data-att="' + esc(a.rel) +
|
|
5206
|
+
'" title="' + esc(t("点击查看")) + '">' + esc(a.nm) + "</span>").join("") + "</div>"
|
|
5027
5207
|
: "") +
|
|
5028
5208
|
"</div></div>";
|
|
5029
5209
|
}
|
|
5030
5210
|
// 协议尾行是给编排器判收的机器标记,不该出现在聊天正文里(真源在提示词约定)
|
|
5031
5211
|
const body = String(it.text || "").replace(/\n*DIRECT_DONE:.*\s*$/s, "").trim();
|
|
5032
|
-
//
|
|
5033
|
-
const
|
|
5212
|
+
// 运行中还没有正文:三点打字动画(终态无正文才落「无文本输出」占位)
|
|
5213
|
+
const bodyHtml = body ? chatBodyHTML(body)
|
|
5214
|
+
: (it.status === "running"
|
|
5215
|
+
? '<span class="chat-typing" aria-label="' + esc(t("正在执行…")) + '"><i></i><i></i><i></i></span>'
|
|
5216
|
+
: esc(t("(本轮无文本输出)")));
|
|
5217
|
+
// 元信息只留 名字+时间(+失败标记):路由/供应商细节属于「步骤」页签,塞这里就是噪音
|
|
5218
|
+
const metaBad = it.status === "failed" || it.status === "cancelled";
|
|
5034
5219
|
// 智能体回复:头像 + 整幅正文(不套气泡框),元信息小字落在正文下方
|
|
5035
5220
|
return '<div class="chat-row">' +
|
|
5036
5221
|
'<span class="chat-avatar" aria-hidden="true"><svg class="ico"><use href="#i-bee"></use></svg></span>' +
|
|
5037
5222
|
'<div class="chat-bubble agent">' +
|
|
5038
|
-
'<
|
|
5039
|
-
'<div class="chat-meta">' + esc(it.who || "") + " " + esc(it.at
|
|
5040
|
-
(
|
|
5041
|
-
(it.status && it.status !== "done" ? " · " + esc(it.status) : "") + "</div></div></div>";
|
|
5223
|
+
'<div class="chat-body">' + bodyHtml + "</div>" +
|
|
5224
|
+
'<div class="chat-meta">' + esc(it.who || "") + " " + esc(chatTime(it.at)) +
|
|
5225
|
+
(metaBad ? " · " + t(it.status === "failed" ? "失败" : "已取消") : "") + "</div></div></div>";
|
|
5042
5226
|
}).join("") || '<div class="hint">' + esc(t("还没有对话内容")) + "</div>";
|
|
5043
5227
|
const hint = $("rd-chat-hint");
|
|
5044
5228
|
if (hint) hint.textContent = active
|
|
5045
|
-
? t("
|
|
5229
|
+
? t("运行中:新消息会排队,本轮回答完后依次送达")
|
|
5046
5230
|
: t("已结束:发送后将自动开新一轮接着做");
|
|
5047
5231
|
flow.scrollTop = flow.scrollHeight;
|
|
5048
5232
|
}
|
|
@@ -5050,12 +5234,28 @@ async function renderChat(run, active) {
|
|
|
5050
5234
|
function drawChatAtts() {
|
|
5051
5235
|
const box = $("rd-chat-att-list");
|
|
5052
5236
|
if (!box) return;
|
|
5237
|
+
// 点名字开预览(待提交区原文走 /api/attachments/<id>),× 才是移除
|
|
5053
5238
|
box.innerHTML = chatAtts.map((a, i) =>
|
|
5054
|
-
'<span class="att-chip2"
|
|
5055
|
-
esc(t("
|
|
5239
|
+
'<span class="att-chip2 att-view" data-att-id="' + esc(a.id || "") +
|
|
5240
|
+
'" data-att-name="' + esc(a.name) + '" title="' + esc(t("点击查看")) + '">' + esc(a.name) +
|
|
5241
|
+
'<b onclick="chatRemoveAtt(' + i + ')" title="' + esc(t("移除")) + '">×' +
|
|
5242
|
+
'</b></span>').join("");
|
|
5056
5243
|
}
|
|
5057
5244
|
window.chatRemoveAtt = function (i) { chatAtts.splice(i, 1); drawChatAtts(); };
|
|
5058
5245
|
|
|
5246
|
+
/* 对话附件点击预览:已落盘的走 run 工作目录 /api/runs/<id>/file(rel 为
|
|
5247
|
+
* _attachments/ 相对路径),待提交的走 /api/attachments/<id>;
|
|
5248
|
+
* 弹窗与成品预览同款——图片直接看图、文本看码、二进制给下载。 */
|
|
5249
|
+
window.chatAttPopup = function (rel) {
|
|
5250
|
+
if (!rel || !chatRunId) return;
|
|
5251
|
+
return _fpPreviewUrl("/api/runs/" + encodeURIComponent(chatRunId) +
|
|
5252
|
+
"/file?name=" + encodeURIComponent(rel), rel);
|
|
5253
|
+
};
|
|
5254
|
+
window.chatPendingPopup = function (id, name) {
|
|
5255
|
+
if (!id) return;
|
|
5256
|
+
return _fpPreviewUrl("/api/attachments/" + encodeURIComponent(id), name || "attachment");
|
|
5257
|
+
};
|
|
5258
|
+
|
|
5059
5259
|
async function chatUploadFiles(files) {
|
|
5060
5260
|
for (const f of files) {
|
|
5061
5261
|
try {
|
|
@@ -5083,24 +5283,33 @@ window.chatSend = async function () {
|
|
|
5083
5283
|
btn.disabled = true;
|
|
5084
5284
|
try {
|
|
5085
5285
|
if (active) {
|
|
5286
|
+
// 运行中:消息进信箱排队(时间线上带「待送达」标记),本轮跑完自动续轮送达
|
|
5086
5287
|
await api("/api/runs/" + encodeURIComponent(chatRunId) + "/messages", {
|
|
5087
5288
|
method: "POST",
|
|
5088
5289
|
body: JSON.stringify({ text, attachments: chatAtts.map((a) => a.id) }),
|
|
5089
5290
|
});
|
|
5090
|
-
toast(t("
|
|
5291
|
+
toast(t("已排队:随下一步一起送达"));
|
|
5091
5292
|
} else {
|
|
5092
5293
|
const r = await api("/api/runs/" + encodeURIComponent(chatRunId) + "/chat", {
|
|
5093
5294
|
method: "POST",
|
|
5094
5295
|
body: JSON.stringify({ text, attachments: chatAtts.map((a) => a.id) }),
|
|
5095
5296
|
});
|
|
5096
5297
|
toast(t("已开新一轮,接着做…"));
|
|
5097
|
-
if (r && r.run_id) {
|
|
5298
|
+
if (r && r.run_id) {
|
|
5299
|
+
if (S.detailTaskKey) { S.taskSig = ""; renderTaskDetail(); }
|
|
5300
|
+
else jumpToRun(r.run_id);
|
|
5301
|
+
}
|
|
5098
5302
|
}
|
|
5303
|
+
// 发送成功一律清空输入框(只清输入,时间线里的历史回复不动);
|
|
5304
|
+
// 此前 /chat 分支提前 return 跳过了清空,发出去的字一直留在框里
|
|
5099
5305
|
ta.value = "";
|
|
5306
|
+
ta.style.height = "";
|
|
5100
5307
|
chatAtts = []; drawChatAtts();
|
|
5101
5308
|
chatSig = ""; // 强制重画时间线
|
|
5102
|
-
|
|
5103
|
-
|
|
5309
|
+
if (active) {
|
|
5310
|
+
const d = await api("/api/runs/" + encodeURIComponent(chatRunId));
|
|
5311
|
+
if (d.run) renderChat(d.run, d.run.status === "running" || d.run.status === "queued");
|
|
5312
|
+
}
|
|
5104
5313
|
} catch (e) { toast(t("发送失败:") + e.message, true); }
|
|
5105
5314
|
finally { btn.disabled = false; }
|
|
5106
5315
|
};
|
|
@@ -5115,6 +5324,12 @@ function bindChat() {
|
|
|
5115
5324
|
e.target.value = "";
|
|
5116
5325
|
});
|
|
5117
5326
|
send.addEventListener("click", window.chatSend);
|
|
5327
|
+
// 自动伸高:跟内容走、有上限(160px 后内部滚动);清空由 chatSend 归零。
|
|
5328
|
+
// 高度只由用户输入驱动,轮询重画不碰它——框框不再忽大忽小
|
|
5329
|
+
ta.addEventListener("input", () => {
|
|
5330
|
+
ta.style.height = "auto";
|
|
5331
|
+
ta.style.height = Math.min(ta.scrollHeight, 160) + "px";
|
|
5332
|
+
});
|
|
5118
5333
|
ta.addEventListener("keydown", (e) => {
|
|
5119
5334
|
if (e.key === "Enter" && !e.shiftKey && !e.isComposing) { e.preventDefault(); window.chatSend(); }
|
|
5120
5335
|
});
|
|
@@ -5128,6 +5343,19 @@ function bindChat() {
|
|
|
5128
5343
|
return f && !f.name ? new File([f], "paste-" + Date.now() + ".png", { type: f.type }) : f;
|
|
5129
5344
|
}).filter(Boolean));
|
|
5130
5345
|
});
|
|
5346
|
+
// 附件胶囊点击预览:委托绑在静态容器上——时间线轮询整块重建 innerHTML、
|
|
5347
|
+
// 输入条胶囊增删重画,都不丢监听。时间线胶囊走已落盘文件,输入条胶囊
|
|
5348
|
+
// (× 移除按钮除外)走待提交区
|
|
5349
|
+
$("rd-chat-flow").addEventListener("click", (e) => {
|
|
5350
|
+
const chip = e.target.closest(".att-open");
|
|
5351
|
+
if (chip) window.chatAttPopup(chip.dataset.att);
|
|
5352
|
+
});
|
|
5353
|
+
const attBox = $("rd-chat-att-list");
|
|
5354
|
+
if (attBox) attBox.addEventListener("click", (e) => {
|
|
5355
|
+
if (e.target.closest("b")) return; // × 自己的 onclick 负责移除
|
|
5356
|
+
const chip = e.target.closest("[data-att-id]");
|
|
5357
|
+
if (chip) window.chatPendingPopup(chip.dataset.attId, chip.dataset.attName);
|
|
5358
|
+
});
|
|
5131
5359
|
}
|
|
5132
5360
|
|
|
5133
5361
|
/* ---------------------------------------------------------- 外观:皮肤 + 明暗(换肤) */
|
|
@@ -5136,6 +5364,7 @@ function bindChat() {
|
|
|
5136
5364
|
* 皮肤改色只需要动 style.css,预览与真实界面不会各自漂移。 */
|
|
5137
5365
|
const SKINS = [
|
|
5138
5366
|
{ id: "ocean", name: "深海", desc: "藏青底色 + 天蓝强调,夜间长时间盯任务更沉静(默认)" },
|
|
5367
|
+
{ id: "hermes", name: "墨金", desc: "暖墨底色 + 金色发丝线,Hermes 式古典优雅" },
|
|
5139
5368
|
{ id: "classic", name: "经典", desc: "黑白灰 + 蓝色强调,ChatGPT 式清爽配色" },
|
|
5140
5369
|
{ id: "forest", name: "森野", desc: "墨绿底色 + 青翠强调,偏自然的护眼配色" },
|
|
5141
5370
|
{ id: "amber", name: "暖阳", desc: "暖棕底色 + 琥珀强调,纸感暖调" },
|
|
@@ -5554,20 +5783,20 @@ function bindSelById(id) {
|
|
|
5554
5783
|
function bindRepaint() { S.bindSig = null; renderBindings(); }
|
|
5555
5784
|
|
|
5556
5785
|
/* 一键推荐绑定:给每个链为空的 CLI 按协议适配规则预填一条主模型。
|
|
5557
|
-
* 推荐顺序:显式协议原生匹配 > auto 且 wire_caps
|
|
5558
|
-
*
|
|
5559
|
-
*
|
|
5786
|
+
* 推荐顺序:显式协议原生匹配 > auto 且 wire_caps 命中;同分按 priority /
|
|
5787
|
+
* 数组序稳定。默认模型停用的厂商直接不推荐——它当前键位就是坏的,绑上去
|
|
5788
|
+
* 解析照样失败;没有合适的就什么都不绑(2026-09-17 用户拍板,不再保留
|
|
5789
|
+
* 「只剩它也上」的兜底)。只预填草稿态(dirty),逐条看清后各自保存
|
|
5560
5790
|
* 或「全部保存」,健康链绝不被覆盖。 */
|
|
5561
5791
|
function recommendFor(c) {
|
|
5562
|
-
|
|
5563
|
-
|
|
5792
|
+
// 目录页「默认模型」下拉用 orch_kind || id 判协议(仅管理条目没有 orch_kind),
|
|
5793
|
+
// 推荐口径与下拉过滤保持一致;绑定页条目恒有 orch_kind,不受影响
|
|
5794
|
+
const allow = bindAllowedProtocols(c.orch_kind || c.id);
|
|
5795
|
+
const badDefault = (p) => (p.models || []).some((m) => m.name === p.model &&
|
|
5796
|
+
(m.hidden || m.enabled === false));
|
|
5797
|
+
const provs = (S.providers || []).filter((p) => !badDefault(p) && chainProvUsable(p, allow));
|
|
5564
5798
|
if (!provs.length) return null;
|
|
5565
|
-
const score = (p) =>
|
|
5566
|
-
const native = allow.indexOf(p.protocol) >= 0;
|
|
5567
|
-
const badDefault = (p.models || []).some((m) => m.name === p.model &&
|
|
5568
|
-
(m.hidden || m.enabled === false)) ? 500 : 0;
|
|
5569
|
-
return (native ? 0 : 1) * 1000 + badDefault + (p.priority || 0);
|
|
5570
|
-
};
|
|
5799
|
+
const score = (p) => (allow.indexOf(p.protocol) >= 0 ? 0 : 1) * 1000 + (p.priority || 0);
|
|
5571
5800
|
provs.sort((a, b) => score(a) - score(b));
|
|
5572
5801
|
const p = provs[0];
|
|
5573
5802
|
const models = (p.models || []).filter((m) => !m.hidden && m.enabled !== false);
|
|
@@ -5605,48 +5834,38 @@ function chainDeadReasons(c, chain) {
|
|
|
5605
5834
|
return dead;
|
|
5606
5835
|
}
|
|
5607
5836
|
|
|
5837
|
+
/* 单条绑定链的推荐修复动作:需要改返回新链,不动返回 null。
|
|
5838
|
+
* 空链 → 预填推荐;整条死透(或只有链首且已死)→ 重推荐;链首死但链内还有
|
|
5839
|
+
* 活的备选 → 新链首插最前(原降级序保留,推荐项已在链内则升首去重)。 */
|
|
5840
|
+
function bindRepairAction(c, st) {
|
|
5841
|
+
const rec = recommendFor(c);
|
|
5842
|
+
if (!st.chain.length) return rec ? [rec] : null;
|
|
5843
|
+
const dead = chainDeadReasons(c, st.chain);
|
|
5844
|
+
const allDead = dead.every((d) => d);
|
|
5845
|
+
const headDead = dead[0] !== ""; // 空 p = CLI 默认凭据,是有意配置不算死
|
|
5846
|
+
if (allDead) return rec ? [rec] : null;
|
|
5847
|
+
if (headDead && rec && chainKey(st.chain) !== chainKey([rec]))
|
|
5848
|
+
return [rec].concat(st.chain.filter((x) => !(x.p === rec.p && x.m === rec.m)));
|
|
5849
|
+
return null;
|
|
5850
|
+
}
|
|
5851
|
+
|
|
5608
5852
|
function autoBindAll() {
|
|
5609
5853
|
const targets = (S.catalog || []).filter((c) => c.installed && c.orch_kind);
|
|
5610
5854
|
let filled = 0, skipped = 0, noProv = 0, refilled = 0;
|
|
5611
5855
|
for (const c of targets) {
|
|
5612
5856
|
const st = bindSelById(c.id);
|
|
5613
|
-
|
|
5614
|
-
|
|
5615
|
-
|
|
5616
|
-
st.chain
|
|
5617
|
-
|
|
5618
|
-
|
|
5857
|
+
const act = bindRepairAction(c, st);
|
|
5858
|
+
if (!act) {
|
|
5859
|
+
// 没动它:分清「健康链无需推荐」和「没有可推荐的」两种落空
|
|
5860
|
+
if (!st.chain.length) { noProv++; continue; }
|
|
5861
|
+
const dead = chainDeadReasons(c, st.chain);
|
|
5862
|
+
if (dead.every((d) => d)) noProv++; else skipped++;
|
|
5619
5863
|
continue;
|
|
5620
5864
|
}
|
|
5621
|
-
|
|
5622
|
-
|
|
5623
|
-
|
|
5624
|
-
|
|
5625
|
-
const allDead = aliveN === 0;
|
|
5626
|
-
const headDead = dead[0] !== ""; // 空 p = CLI 默认凭据,是有意配置不算死
|
|
5627
|
-
const rec = recommendFor(c);
|
|
5628
|
-
if (allDead && rec) {
|
|
5629
|
-
st.chain = [rec];
|
|
5630
|
-
st.dirty = true;
|
|
5631
|
-
refilled++;
|
|
5632
|
-
} else if (allDead) {
|
|
5633
|
-
noProv++;
|
|
5634
|
-
} else if (headDead && rec && chainKey(st.chain) !== chainKey([rec])) {
|
|
5635
|
-
// 链首死但链内还有活的备选:只把推荐的新链首插到最前面(原降级序保留)
|
|
5636
|
-
const hasRec = st.chain.some((x) => x.p === rec.p && x.m === rec.m);
|
|
5637
|
-
if (!hasRec) {
|
|
5638
|
-
st.chain = [rec].concat(st.chain);
|
|
5639
|
-
st.dirty = true;
|
|
5640
|
-
refilled++;
|
|
5641
|
-
} else {
|
|
5642
|
-
// 推荐项已在链内:把它升到链首
|
|
5643
|
-
st.chain = [rec].concat(st.chain.filter((x) => x !== rec));
|
|
5644
|
-
st.dirty = true;
|
|
5645
|
-
refilled++;
|
|
5646
|
-
}
|
|
5647
|
-
} else {
|
|
5648
|
-
skipped++;
|
|
5649
|
-
}
|
|
5865
|
+
const wasEmpty = !st.chain.length;
|
|
5866
|
+
st.chain = act;
|
|
5867
|
+
st.dirty = true;
|
|
5868
|
+
if (wasEmpty) filled++; else refilled++;
|
|
5650
5869
|
}
|
|
5651
5870
|
bindRepaint();
|
|
5652
5871
|
if (filled && refilled) {
|
|
@@ -5664,6 +5883,56 @@ function autoBindAll() {
|
|
|
5664
5883
|
}
|
|
5665
5884
|
}
|
|
5666
5885
|
|
|
5886
|
+
/* 厂商/模型停用·启用·删除后自动补一次推荐绑定(2026-09-17 用户拍板):
|
|
5887
|
+
* 绑定链——空链预填、死链重推荐、死链首插新首,与「一键推荐绑定」同规则,
|
|
5888
|
+
* 但直接落盘(自动场景没有人工确认环节);目录页——空默认模型直填。
|
|
5889
|
+
* 没有合适的推荐就保持原样,什么都不绑。绑定页上用户手改中的草稿(dirty)
|
|
5890
|
+
* 不碰;一处都没改成静默返回,不打扰停用/启用的操作反馈。 */
|
|
5891
|
+
let _autoRebindRunning = false, _autoRebindAgain = false;
|
|
5892
|
+
async function autoRebindSoon() {
|
|
5893
|
+
if (_autoRebindRunning) { _autoRebindAgain = true; return; }
|
|
5894
|
+
_autoRebindRunning = true;
|
|
5895
|
+
try {
|
|
5896
|
+
await poll(); // 拿停用/启用落盘后的最新 providers / bindings / catalog
|
|
5897
|
+
let fixed = 0;
|
|
5898
|
+
for (const c of (S.catalog || []).filter((x) => x.installed && x.orch_kind)) {
|
|
5899
|
+
const st = bindSelById(c.id);
|
|
5900
|
+
if (st.dirty) continue; // 用户手改中,不覆盖草稿
|
|
5901
|
+
const act = bindRepairAction(c, st);
|
|
5902
|
+
if (!act) continue;
|
|
5903
|
+
const b = (S.bindings || {})[c.id] || {};
|
|
5904
|
+
try {
|
|
5905
|
+
await api("/api/models/binding", { method: "POST", body: JSON.stringify({
|
|
5906
|
+
agent_id: c.id, provider_id: act[0].p,
|
|
5907
|
+
chain: act.map((x) => ({ provider_id: x.p, model: x.m })),
|
|
5908
|
+
difficulty_routing: !!b.difficulty_routing }) });
|
|
5909
|
+
st.chain = act; st.dirty = false; st.key = chainKey(act);
|
|
5910
|
+
fixed++;
|
|
5911
|
+
} catch (e) { /* 单条失败不打断,等下次变更再补 */ }
|
|
5912
|
+
}
|
|
5913
|
+
for (const c of (S.catalog || []).filter((x) =>
|
|
5914
|
+
x.installed && x.config_writable && !fmtModel(x.model))) {
|
|
5915
|
+
const rec = recommendFor(c);
|
|
5916
|
+
if (!rec) continue;
|
|
5917
|
+
try {
|
|
5918
|
+
const r = await api("/api/catalog/" + encodeURIComponent(c.id) + "/model",
|
|
5919
|
+
{ method: "POST", body: JSON.stringify({ model: rec.m }) });
|
|
5920
|
+
c.model = r.model || rec.m;
|
|
5921
|
+
fixed++;
|
|
5922
|
+
} catch (e) { /* 同上 */ }
|
|
5923
|
+
}
|
|
5924
|
+
if (fixed) {
|
|
5925
|
+
S.catSig = null; S.bindSig = null;
|
|
5926
|
+
render();
|
|
5927
|
+
toast(t("厂商/模型变动,已自动重绑 %1 处。").replace("%1", fixed));
|
|
5928
|
+
}
|
|
5929
|
+
} catch (e) { /* 自动补绑失败静默:不打断用户的停用/启用操作 */ }
|
|
5930
|
+
finally {
|
|
5931
|
+
_autoRebindRunning = false;
|
|
5932
|
+
if (_autoRebindAgain) { _autoRebindAgain = false; autoRebindSoon(); }
|
|
5933
|
+
}
|
|
5934
|
+
}
|
|
5935
|
+
|
|
5667
5936
|
async function saveAllBindings() {
|
|
5668
5937
|
const targets = (S.catalog || []).filter((c) => c.installed && c.orch_kind);
|
|
5669
5938
|
const dirty = targets.filter((c) => (S.bindSel[c.id] || {}).dirty);
|
|
@@ -5698,6 +5967,8 @@ function bindModelBox(c, provId) {
|
|
|
5698
5967
|
: ""));
|
|
5699
5968
|
return '<span class="ochip' + (i === 0 ? " primary" : "") + '">' +
|
|
5700
5969
|
"<b>" + (i === 0 ? t("主") : t("备")) + "</b>" + esc(pname) + " · " + esc(c2.m) +
|
|
5970
|
+
(modelHasImage(c2.p, c2.m)
|
|
5971
|
+
? ' <span class="tag ok" title="' + esc(t("支持图片输入")) + '">' + t("图") + "</span>" : "") +
|
|
5701
5972
|
(dead ? ' <span class="hint warn">⚠ ' + esc(t("协议不匹配,解析时跳过")) + "</span>" : badge) +
|
|
5702
5973
|
(i > 0 ? '<button class="mini" data-m="' + esc(c2.m) + '" data-p="' + esc(c2.p) +
|
|
5703
5974
|
'" title="' + t("设为主模型") + '" onclick="bindPromote(\'' + esc(c.id) + '\', this)">' +
|
|
@@ -5707,7 +5978,7 @@ function bindModelBox(c, provId) {
|
|
|
5707
5978
|
"</span>";
|
|
5708
5979
|
}).join("")
|
|
5709
5980
|
: '<span class="hint">' + t("未设置") + (provId
|
|
5710
|
-
? t("
|
|
5981
|
+
? t("(按供应商/难度自动解析——供应商协议不匹配或被停用时解析为空,相关步骤将判失败)")
|
|
5711
5982
|
: t("(用 CLI 默认模型——不会注入任何供应商凭据)")) + "</span>";
|
|
5712
5983
|
return '<div class="field"><label>' + t("运行时模型链(跨厂商,最多 ") + MAX_ORCH_MODELS + t(" 条)") + "</label>" +
|
|
5713
5984
|
'<div class="orch-row">' + chips +
|
|
@@ -5750,6 +6021,8 @@ function bindPanel(c) {
|
|
|
5750
6021
|
return '<label class="oitem' + (dead ? " dim" : "") + '"><input type="checkbox" value="' + esc(m) + '" data-p="' + esc(g.id) + '"' +
|
|
5751
6022
|
(has ? " checked" : "") + (dead ? " disabled" : "") +
|
|
5752
6023
|
" onchange=\"bindPick('" + esc(c.id) + "', this, this.checked)\">" + esc(m) +
|
|
6024
|
+
(modelHasImage(g.id, m)
|
|
6025
|
+
? ' <span class="tag ok" title="' + esc(t("支持图片输入")) + '">' + t("图") + "</span>" : "") +
|
|
5753
6026
|
(dead ? ' <span class="hint warn">' + esc(t("无可用 wire,解析时跳过")) + "</span>" : "") +
|
|
5754
6027
|
"</label>";
|
|
5755
6028
|
}).join("") +
|
|
@@ -5895,6 +6168,45 @@ async function saveModel(id) {
|
|
|
5895
6168
|
poll();
|
|
5896
6169
|
}
|
|
5897
6170
|
|
|
6171
|
+
/* 一键绑定推荐模型(智能体目录页):给「默认模型」还空着的已安装智能体按
|
|
6172
|
+
* 协议适配规则(复用绑定页 recommendFor 评分)挑一家厂商的启用模型并直接
|
|
6173
|
+
* 写入该 CLI 自身配置(写入侧先自动 .bak 备份)。已配置的不动——目录页的
|
|
6174
|
+
* 默认模型会写进 CLI 全局配置,手工设置过的值(含 CLI 订阅自带的默认)不
|
|
6175
|
+
* 该被一键覆盖;要换模型逐卡下拉改就行。 */
|
|
6176
|
+
async function catAutoBindAll() {
|
|
6177
|
+
const targets = (S.catalog || []).filter((c) => c.installed && c.config_writable);
|
|
6178
|
+
if (!targets.length) { toast(t("没有支持写入默认模型的已安装智能体。"), true); return; }
|
|
6179
|
+
const empty = targets.filter((c) => !fmtModel(c.model));
|
|
6180
|
+
if (!empty.length) { toast(t("所有已安装智能体都已配置默认模型,无需绑定。")); return; }
|
|
6181
|
+
let done = 0, failed = 0, noProv = 0, firstErr = "";
|
|
6182
|
+
for (const c of empty) {
|
|
6183
|
+
const rec = recommendFor(c);
|
|
6184
|
+
if (!rec) { noProv++; continue; }
|
|
6185
|
+
try {
|
|
6186
|
+
const r = await api("/api/catalog/" + encodeURIComponent(c.id) + "/model",
|
|
6187
|
+
{ method: "POST", body: JSON.stringify({ model: rec.m }) });
|
|
6188
|
+
c.model = r.model || rec.m;
|
|
6189
|
+
done++;
|
|
6190
|
+
} catch (e) {
|
|
6191
|
+
failed++;
|
|
6192
|
+
if (!firstErr) firstErr = c.name + ":" + e.message;
|
|
6193
|
+
}
|
|
6194
|
+
}
|
|
6195
|
+
S.catSig = null;
|
|
6196
|
+
renderCatalog();
|
|
6197
|
+
if (done && failed) {
|
|
6198
|
+
toast(t("已为 %1 个智能体写入推荐模型,%2 个失败。").replace("%1", done).replace("%2", failed) +
|
|
6199
|
+
(firstErr ? " " + firstErr : ""), true);
|
|
6200
|
+
} else if (done) {
|
|
6201
|
+
toast(t("已为 %1 个智能体写入推荐模型(原配置已自动备份 .bak)。").replace("%1", done));
|
|
6202
|
+
} else if (failed) {
|
|
6203
|
+
toast(t("推荐模型写入失败:") + firstErr, true);
|
|
6204
|
+
} else {
|
|
6205
|
+
toast(t("没有可推荐的:先到「模型接入」页导入与 CLI 协议匹配的供应商。"), true);
|
|
6206
|
+
}
|
|
6207
|
+
poll();
|
|
6208
|
+
}
|
|
6209
|
+
|
|
5898
6210
|
/* 一键打开:web 类后台起服务并自动开浏览器;console 类新开终端窗口跑交互 TUI。
|
|
5899
6211
|
用的模型就是目录页「默认模型」已写入该 CLI 配置文件的那份;密钥由服务端按编排同款规则注入 */
|
|
5900
6212
|
async function openAgent(id) {
|
|
@@ -6957,9 +7269,24 @@ async function loadSelfupdate(force) {
|
|
|
6957
7269
|
SU = await api("/api/selfupdate" + (force ? "?force=1" : ""));
|
|
6958
7270
|
} catch (e) { SU = null; }
|
|
6959
7271
|
renderSu();
|
|
7272
|
+
maybeWhatsnew();
|
|
6960
7273
|
return SU;
|
|
6961
7274
|
}
|
|
6962
7275
|
|
|
7276
|
+
/* 升级重启后的一次性「本次更新内容」:localStorage 记住已展示的版本,
|
|
7277
|
+
* 版本变化且有本地 changelog 小节时弹窗(首次使用只记账不弹)。 */
|
|
7278
|
+
function maybeWhatsnew() {
|
|
7279
|
+
if (!SU || !SU.current) return;
|
|
7280
|
+
const prev = localStorage.getItem("su.myver");
|
|
7281
|
+
localStorage.setItem("su.myver", SU.current);
|
|
7282
|
+
if (prev && prev !== SU.current && SU.whatsnew) {
|
|
7283
|
+
openModal(t("本次更新内容"),
|
|
7284
|
+
"<p class=\"hint\">" + t("已更新到") + " <b>v" + esc(SU.current) + "</b></p>" +
|
|
7285
|
+
"<pre class=\"su-notes\">" + esc(SU.whatsnew) + "</pre>",
|
|
7286
|
+
"<button class=\"small\" onclick=\"closeModal()\">" + t("知道了") + "</button>");
|
|
7287
|
+
}
|
|
7288
|
+
}
|
|
7289
|
+
|
|
6963
7290
|
function renderSu() {
|
|
6964
7291
|
const info = $("su-info");
|
|
6965
7292
|
if (!info) return;
|
|
@@ -6970,6 +7297,10 @@ function renderSu() {
|
|
|
6970
7297
|
if (SU.has_update) {
|
|
6971
7298
|
html += "<p class=\"hint\"><b>" + t("发现新版本") + " v" + SU.latest +
|
|
6972
7299
|
t(" ") + "<a href=\"#\" onclick=\"event.preventDefault();suApply()\">" + t("立即升级") + "</a></b></p>";
|
|
7300
|
+
if (SU.notes) {
|
|
7301
|
+
html += "<div class=\"hint\"><b>" + t("新版本更新内容") + t(":") + "</b>" +
|
|
7302
|
+
"<pre class=\"su-notes\">" + esc(SU.notes) + "</pre></div>";
|
|
7303
|
+
}
|
|
6973
7304
|
} else if (SU.mode === "npm" && !SU.note) {
|
|
6974
7305
|
html += "<p class=\"hint\">" + t("已是最新版。") + "</p>";
|
|
6975
7306
|
}
|
|
@@ -8028,6 +8359,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
8028
8359
|
$("btn-reload-catalog").addEventListener("click", async () => {
|
|
8029
8360
|
await api("/api/catalog/reload", { method: "POST" }); poll(); refreshSessionAgents();
|
|
8030
8361
|
});
|
|
8362
|
+
$("btn-catalog-autobind").addEventListener("click", catAutoBindAll);
|
|
8031
8363
|
$("btn-import").addEventListener("click", openImportDialog);
|
|
8032
8364
|
$("btn-add-provider").addEventListener("click", openAddProviderDialog);
|
|
8033
8365
|
$("prov-search").addEventListener("input", () => {
|