codebee 0.1.5 → 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/app/ui/app.js CHANGED
@@ -567,7 +567,9 @@ function renderHealthBanner(health) {
567
567
  el.className = "health-banner alerting";
568
568
  el.title = alerts.map((p) =>
569
569
  p.provider + (p.model ? " · " + p.model : "") +
570
- t(":连续失败 ") + p.consecutive_failures + t(" 次(") + (p.last_error || t("未知错误")) + t(")")
570
+ (p.static
571
+ ? t(":") + (p.last_error || t("未知错误"))
572
+ : t(":连续失败 ") + p.consecutive_failures + t(" 次(") + (p.last_error || t("未知错误")) + t(")"))
571
573
  ).join("\n");
572
574
  if (!_healthBeeped) { _healthBeeped = true; beepAttention(); }
573
575
  }
@@ -575,7 +577,7 @@ function renderHealthBanner(health) {
575
577
  function healthBannerText(health) {
576
578
  return ((health && health.alerts) || []).map((p) => {
577
579
  const m = p.model ? " · " + p.model : "";
578
- return "⚠ " + p.provider + m + " " + t("连接异常");
580
+ return "⚠ " + p.provider + m + " " + (p.static ? t("绑定链失效") : t("连接异常"));
579
581
  }).join(t(" "));
580
582
  }
581
583
 
@@ -584,19 +586,21 @@ function healthBannerClick() {
584
586
  if (!alerts.length) return;
585
587
  const rows = alerts.map((p) => {
586
588
  const model = p.model ? " · " + p.model : "";
587
- return "<div style='margin-bottom:10px'>" +
588
- "<b>" + esc(p.provider) + (p.model ? " · " + esc(p.model) : "") + "</b>" +
589
+ const statLine = p.static ? "" :
589
590
  "<div style='opacity:.75;font-size:12px;margin-top:2px'>" +
590
591
  t("连续失败") + " " + p.consecutive_failures + " " + t("次 · 首次失败 ") + (p.first_fail_at || "-") +
591
- "</div>" +
592
+ "</div>";
593
+ return "<div style='margin-bottom:10px'>" +
594
+ "<b>" + esc(p.provider) + (p.model ? " · " + esc(p.model) : "") + "</b>" +
595
+ statLine +
592
596
  "<div style='color:#dc2626;font-size:12px;margin-top:2px;word-break:break-all'>" +
593
597
  esc(p.last_error || "") + "</div></div>";
594
598
  }).join("");
595
599
  const pid0 = alerts[0].provider_id || "";
596
600
  const model0 = alerts[0].model || "";
597
601
  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>" +
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>" : "") +
600
604
  "<button class='btn ghost' onclick=\"healthOp('silence')\">" + t("静默本次告警") + "</button>" +
601
605
  "<button class='btn ghost' onclick=\"healthOp('reset')\">" + t("手动标记恢复") + "</button>" +
602
606
  "<button class='primary' onclick='closeModal()'>" + t("关闭") + "</button>";
@@ -640,6 +644,7 @@ async function healthDisableModel(pid, model) {
640
644
  S.provSig = "";
641
645
  closeModal(); render(); loadOrchestrator();
642
646
  toast(t("已禁用模型 {0} · {1},链降级自动跳过;绑定页可重新启用").replace("{0}", pid).replace("{1}", model));
647
+ autoRebindSoon();
643
648
  } catch (e) {
644
649
  toast(t("操作失败:") + e.message, true);
645
650
  }
@@ -663,6 +668,7 @@ async function healthDisableProvider(pid) {
663
668
  S.provSig = "";
664
669
  closeModal(); render(); loadOrchestrator();
665
670
  toast(t("已禁用厂商 {0}:链降级自动跳过,绑定页可重新启用").replace("{0}", pid));
671
+ autoRebindSoon();
666
672
  } catch (e) {
667
673
  toast(t("操作失败:") + e.message, true);
668
674
  }
@@ -848,19 +854,19 @@ function renderBindings() {
848
854
  }).join("");
849
855
  const bound = provs.find((p) => p.id === b.provider_id);
850
856
  const offWarn = bound && bound.enabled === false
851
- ? '<p class="hint warn">' + t("该供应商已停用:编排时不会注入它,将回落为 CLI 默认配置(模型链也不会生效)。") + '</p>' : "";
857
+ ? '<p class="hint warn">' + t("该供应商已停用:编排时不会注入它,模型链因此失效时相关步骤会直接判失败。") + '</p>' : "";
852
858
  const protoWarn = bound && !bindable.some((p) => p.id === bound.id)
853
859
  ? '<p class="hint warn">' + t("该供应商协议为 ") + esc(protoLabel(bound)) +
854
- t(",当前没有可注入的 CLI,编排时会回落为 CLI 默认配置。") + '</p>' : "";
855
- // 供应商停用但链上勾选了它家模型:链条目在解析时也会被整条跳过,
856
- // 链空了照样回落本机默认——这是 2026-09-16 配额事故的隐藏形态,单说
857
- // 「供应商下拉」警告不够,链本身的死活也要点名。
860
+ t(",当前没有可注入的 CLI,编排时相关步骤会直接判失败。") + '</p>' : "";
861
+ // 供应商停用但链上勾选了它家模型:链条目在解析时也会被整条跳过,链空了
862
+ // 相关步骤会直接判失败(2026-09-17 起;此前的静默回落本机默认正是 2026-09-16
863
+ // 配额事故的隐藏形态)——单说「供应商下拉」警告不够,链本身的死活也要点名。
858
864
  const chainDead = bindChain(b).some((c2) => c2.p && (() => {
859
865
  const pv = provs.find((p) => p.id === c2.p);
860
866
  return !pv || pv.enabled === false;
861
867
  })());
862
868
  const chainWarn = (!bound || bound.enabled !== false) && chainDead
863
- ? '<p class="hint warn">' + t("模型链里有已停用/已删除的供应商:这些条目解析时会被跳过,链可能因此整体失效。") + '</p>' : "";
869
+ ? '<p class="hint warn">' + t("模型链里有已停用/已删除的供应商:这些条目解析时会被跳过,链可能因此整体失效,相关步骤将判失败。") + '</p>' : "";
864
870
  return '<div class="card"><div class="head"><span class="name">' + esc(c.name) + "</span>" +
865
871
  '<span class="tag">' + esc(c.orch_kind) + "</span></div>" +
866
872
  '<div class="field"><label>' + t("供应商") + '</label><select id="bindprov-' + esc(c.id) + '">' + opts + "</select></div>" +
@@ -964,6 +970,7 @@ async function batchProvOp(op) {
964
970
  try {
965
971
  await api("/api/models/provider-op", { method: "POST",
966
972
  body: JSON.stringify({ ids, op }) });
973
+ if (op !== "duplicate") autoRebindSoon();
967
974
  } catch (e) { toast(t("操作失败:") + e.message, true); }
968
975
  S.selProvs = {};
969
976
  S.modelsSig = null;
@@ -1150,6 +1157,7 @@ async function keyOpCall(pid, op, keyId, key, label) {
1150
1157
  try {
1151
1158
  await api("/api/models/key-op", { method: "POST",
1152
1159
  body: JSON.stringify({ provider_id: pid, op, key_id: keyId, key, label }) });
1160
+ autoRebindSoon(); // 密钥启停/删除/解冻会改变厂商「推荐可用」判定,自动补绑一次
1153
1161
  } catch (e) { toast(t("操作失败:") + e.message, true); }
1154
1162
  // 操作期间轮询照跑(uiPrompt 弹框不阻塞轮询),S.modelsSig 还是旧值:必须
1155
1163
  // 清签名强制重绘,否则 KEY 写盘成功界面也看不到。
@@ -1359,6 +1367,7 @@ async function modelOp(pid, name, op) {
1359
1367
  try {
1360
1368
  await api("/api/models/model-op", { method: "POST",
1361
1369
  body: JSON.stringify({ provider_id: pid, name, op }) });
1370
+ autoRebindSoon(); // 模型停用/启用/删除会改变推荐源与死链判定,自动补绑一次
1362
1371
  const s = modelSel(pid); // 单行操作后同步清掉该行勾选
1363
1372
  if (s[name]) { delete modelSelSet(pid)[name]; }
1364
1373
  } catch (e) { toast(t("操作失败:") + e.message, true); }
@@ -1425,6 +1434,7 @@ async function batchModelOp(pid, op) {
1425
1434
  try {
1426
1435
  await api("/api/models/model-op", { method: "POST",
1427
1436
  body: JSON.stringify({ provider_id: pid, names, op }) });
1437
+ autoRebindSoon();
1428
1438
  } catch (e) { toast(t("操作失败:") + e.message, true); }
1429
1439
  clearModelSel(pid);
1430
1440
  poll();
@@ -1443,6 +1453,7 @@ async function toggleProviderEnabled(pid, enabled) {
1443
1453
  try {
1444
1454
  await api("/api/models/provider-op", { method: "POST",
1445
1455
  body: JSON.stringify({ ids: [pid], op: off ? "enable" : "disable" }) });
1456
+ autoRebindSoon();
1446
1457
  } catch (e) { toast(t("操作失败:") + e.message, true); }
1447
1458
  S.modelsSig = null;
1448
1459
  poll();
@@ -1466,7 +1477,8 @@ async function refreshAllModels() {
1466
1477
  btn.disabled = false; btn.classList.remove("loading");
1467
1478
  btn.title = t("全部重新拉取模型列表");
1468
1479
  }, 1500);
1469
- setTimeout(poll, 2500); setTimeout(poll, 8000);
1480
+ setTimeout(poll, 2500);
1481
+ setTimeout(autoRebindSoon, 8000); // 模型列表落盘后推荐源才完整,补绑一次
1470
1482
  }
1471
1483
 
1472
1484
  async function refreshProviderModels(id) {
@@ -1477,7 +1489,7 @@ async function refreshProviderModels(id) {
1477
1489
  else toast(t("已获取模型列表 · wire 适配测试后台进行中"));
1478
1490
  } catch (e) { toast(t("获取失败:") + e.message, true); }
1479
1491
  poll();
1480
- setTimeout(poll, 4000); // 后台探测落盘后补一轮,适配徽标及时出现
1492
+ setTimeout(autoRebindSoon, 4000); // 模型列表落盘后补绑一次(后台 wire 探测同步进行)
1481
1493
  }
1482
1494
 
1483
1495
  /* 供应商编辑卡:协议可选「自动」;显式选定时它是主协议(用于需要唯一协议的
@@ -1533,6 +1545,7 @@ async function delProvider(id) {
1533
1545
  try {
1534
1546
  await api("/api/models/provider-op", { method: "POST",
1535
1547
  body: JSON.stringify({ ids: [id], op: "delete" }) });
1548
+ autoRebindSoon();
1536
1549
  } catch (e) { toast(t("操作失败:") + e.message, true); }
1537
1550
  if (S.selModels) delete S.selModels[id];
1538
1551
  if (S.selProvs) delete S.selProvs[id];
@@ -1596,6 +1609,7 @@ async function doAddProvider() {
1596
1609
  S.modelsSig = null;
1597
1610
  closeModal();
1598
1611
  poll();
1612
+ autoRebindSoon(); // 新增即启用:马上给空链/死链一次推荐机会
1599
1613
  if (p && body.api_key) refreshProviderModels(p.id); // 有密钥才自动拉模型列表
1600
1614
  } catch (e) {
1601
1615
  res.textContent = t("保存失败:") + e.message;
@@ -1691,7 +1705,7 @@ async function doImport() {
1691
1705
  btn.textContent = t("完成");
1692
1706
  btn.disabled = false;
1693
1707
  btn.onclick = closeModal;
1694
- if (r.imported) { S.selProv = null; S.modelsSig = null; poll(); }
1708
+ if (r.imported) { S.selProv = null; S.modelsSig = null; poll(); autoRebindSoon(); }
1695
1709
  } catch (e) {
1696
1710
  $("import-result").innerHTML = '<div class="msg bad">' + t("导入失败:") + esc(e.message) + "</div>";
1697
1711
  btn.disabled = false; btn.textContent = t("导入选中");
@@ -4805,7 +4819,9 @@ window.renderHive = function (run) {
4805
4819
  const key = run.id + "|" + (s.log || "");
4806
4820
  if (st !== "running") delete hiveTails[key];
4807
4821
  const tailText = st === "running" ? (hiveTails[key] || concl) : concl;
4808
- const title = [s.role, who, s.started_at ? t("开始于 ") + s.started_at : "",
4822
+ const title = [s.role, who,
4823
+ (s.duration_s != null ? s.duration_s + "s" : ""),
4824
+ s.started_at ? t("开始于 ") + s.started_at : "",
4809
4825
  (s.note ? "◆ " + s.note : "")].filter(Boolean).join(" · ")
4810
4826
  + (concl && st !== "running" ? "\n" + t("结论:") + concl : "");
4811
4827
  // --d:入场瀑布逐格延迟(泳道间 110ms + 同泳道逐格 45ms),样式端消费
@@ -4814,7 +4830,7 @@ window.renderHive = function (run) {
4814
4830
  ' title="' + esc(title) + '" ' +
4815
4831
  'onclick="hiveOpenLog(\'' + esc(run.id) + "', '" + esc(s.log || "") + '\')">' +
4816
4832
  '<div class="hc-head">' +
4817
- '<svg class="ico hc-bee" aria-hidden="true"><use href="#i-bee"></use></svg>' +
4833
+ '<img class="hc-bee" src="icons/bee.svg" alt="" aria-hidden="true">' +
4818
4834
  '<span class="hc-role">' + esc(s.role || "") + "</span>" +
4819
4835
  '<span class="hc-who">' + esc(who) + "</span></div>" +
4820
4836
  '<div class="hc-tail" data-log="' + esc(s.log || "") + '">' +
@@ -4852,7 +4868,13 @@ window.renderHive = function (run) {
4852
4868
  (hasRun ? '<span class="lane-live"><i class="live-dot"></i>' + t("进行中") + "</span>" : "") + "</div>" +
4853
4869
  '<div class="lane-track">' + dots(list, i) + "</div>" +
4854
4870
  '<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"></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>');
4856
4878
  // 入场瀑布闸门:只按"结构"(泳道数/格数/状态构成)签名;运行中尾巴/秒表每 2s
4857
4879
  // 变化不能触重播。签名没变就不动 DOM——格子不闪、动画不重启、悬停不弹手。
4858
4880
  const sig = lanes.map((l) => l + ":" + byStage[l].length + "(" +
@@ -5761,20 +5783,20 @@ function bindSelById(id) {
5761
5783
  function bindRepaint() { S.bindSig = null; renderBindings(); }
5762
5784
 
5763
5785
  /* 一键推荐绑定:给每个链为空的 CLI 按协议适配规则预填一条主模型。
5764
- * 推荐顺序:显式协议原生匹配 > auto 且 wire_caps 命中;默认模型停用的厂商
5765
- * 降权(当前键位是坏的,除非只剩它,否则不当首选——失效链重推荐实测);
5766
- * 同分按 priority / 数组序稳定。只预填草稿态(dirty),逐条看清后各自保存
5786
+ * 推荐顺序:显式协议原生匹配 > auto 且 wire_caps 命中;同分按 priority /
5787
+ * 数组序稳定。默认模型停用的厂商直接不推荐——它当前键位就是坏的,绑上去
5788
+ * 解析照样失败;没有合适的就什么都不绑(2026-09-17 用户拍板,不再保留
5789
+ * 「只剩它也上」的兜底)。只预填草稿态(dirty),逐条看清后各自保存
5767
5790
  * 或「全部保存」,健康链绝不被覆盖。 */
5768
5791
  function recommendFor(c) {
5769
- const allow = bindAllowedProtocols(c.orch_kind);
5770
- const provs = (S.providers || []).filter((p) => chainProvUsable(p, allow));
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));
5771
5798
  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
- };
5799
+ const score = (p) => (allow.indexOf(p.protocol) >= 0 ? 0 : 1) * 1000 + (p.priority || 0);
5778
5800
  provs.sort((a, b) => score(a) - score(b));
5779
5801
  const p = provs[0];
5780
5802
  const models = (p.models || []).filter((m) => !m.hidden && m.enabled !== false);
@@ -5812,48 +5834,38 @@ function chainDeadReasons(c, chain) {
5812
5834
  return dead;
5813
5835
  }
5814
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
+
5815
5852
  function autoBindAll() {
5816
5853
  const targets = (S.catalog || []).filter((c) => c.installed && c.orch_kind);
5817
5854
  let filled = 0, skipped = 0, noProv = 0, refilled = 0;
5818
5855
  for (const c of targets) {
5819
5856
  const st = bindSelById(c.id);
5820
- if (!st.chain.length) {
5821
- const rec = recommendFor(c);
5822
- if (!rec) { noProv++; continue; }
5823
- st.chain = [rec];
5824
- st.dirty = true;
5825
- filled++;
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++;
5826
5863
  continue;
5827
5864
  }
5828
- // 链非空:只处理死链——整条链全部死透(或只有链首且已死)才重推荐,
5829
- // 部分存活的长链保留(还降级得动,不擅自覆盖用户手工配的结构)
5830
- const dead = chainDeadReasons(c, st.chain);
5831
- const aliveN = dead.filter((d) => !d).length;
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
- }
5865
+ const wasEmpty = !st.chain.length;
5866
+ st.chain = act;
5867
+ st.dirty = true;
5868
+ if (wasEmpty) filled++; else refilled++;
5857
5869
  }
5858
5870
  bindRepaint();
5859
5871
  if (filled && refilled) {
@@ -5871,6 +5883,56 @@ function autoBindAll() {
5871
5883
  }
5872
5884
  }
5873
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
+
5874
5936
  async function saveAllBindings() {
5875
5937
  const targets = (S.catalog || []).filter((c) => c.installed && c.orch_kind);
5876
5938
  const dirty = targets.filter((c) => (S.bindSel[c.id] || {}).dirty);
@@ -5916,7 +5978,7 @@ function bindModelBox(c, provId) {
5916
5978
  "</span>";
5917
5979
  }).join("")
5918
5980
  : '<span class="hint">' + t("未设置") + (provId
5919
- ? t("(按供应商/难度自动解析——供应商协议不匹配或被停用时解析为空,回落 CLI 本机默认)")
5981
+ ? t("(按供应商/难度自动解析——供应商协议不匹配或被停用时解析为空,相关步骤将判失败)")
5920
5982
  : t("(用 CLI 默认模型——不会注入任何供应商凭据)")) + "</span>";
5921
5983
  return '<div class="field"><label>' + t("运行时模型链(跨厂商,最多 ") + MAX_ORCH_MODELS + t(" 条)") + "</label>" +
5922
5984
  '<div class="orch-row">' + chips +
@@ -6106,6 +6168,45 @@ async function saveModel(id) {
6106
6168
  poll();
6107
6169
  }
6108
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
+
6109
6210
  /* 一键打开:web 类后台起服务并自动开浏览器;console 类新开终端窗口跑交互 TUI。
6110
6211
  用的模型就是目录页「默认模型」已写入该 CLI 配置文件的那份;密钥由服务端按编排同款规则注入 */
6111
6212
  async function openAgent(id) {
@@ -8258,6 +8359,7 @@ document.addEventListener("DOMContentLoaded", () => {
8258
8359
  $("btn-reload-catalog").addEventListener("click", async () => {
8259
8360
  await api("/api/catalog/reload", { method: "POST" }); poll(); refreshSessionAgents();
8260
8361
  });
8362
+ $("btn-catalog-autobind").addEventListener("click", catAutoBindAll);
8261
8363
  $("btn-import").addEventListener("click", openImportDialog);
8262
8364
  $("btn-add-provider").addEventListener("click", openAddProviderDialog);
8263
8365
  $("prov-search").addEventListener("input", () => {
package/app/ui/i18n.js CHANGED
@@ -577,8 +577,8 @@
577
577
  "(用 CLI 默认模型)": " (use CLI default model)",
578
578
  "(用 CLI 默认模型——不会注入任何供应商凭据)": " (use CLI default model — no provider credentials injected)",
579
579
  "(按供应商/难度自动解析)": " (auto by provider/difficulty)",
580
- "(按供应商/难度自动解析——供应商协议不匹配或被停用时解析为空,回落 CLI 本机默认)": " (auto by provider/difficulty — resolves to empty and falls back to CLI defaults if the provider's protocol mismatches or is disabled)",
581
- "模型链里有已停用/已删除的供应商:这些条目解析时会被跳过,链可能因此整体失效。": "The model chain contains disabled/deleted providers: those entries are skipped at resolve time, which can invalidate the whole chain.",
580
+ "(按供应商/难度自动解析——供应商协议不匹配或被停用时解析为空,相关步骤将判失败)": " (auto by provider/difficulty — resolves to empty and the step fails hard if the provider's protocol mismatches or is disabled)",
581
+ "模型链里有已停用/已删除的供应商:这些条目解析时会被跳过,链可能因此整体失效,相关步骤将判失败。": "The model chain contains disabled/deleted providers: those entries are skipped at resolve time, which can invalidate the whole chain and make affected steps fail hard.",
582
582
  "名称": "Name",
583
583
  "默认模型": "Default model",
584
584
  "API 地址": "API endpoint",
@@ -839,7 +839,7 @@
839
839
  "重试": "Retry",
840
840
  ",点「升级到新版」即可": " — click \"Upgrade to new version\" to proceed",
841
841
  "后点重新加载。": " and click Reload.",
842
- "启用「参与编排」后,该智能体会出现在任务表单与评审组中。进入本页会自动检查各 CLI 是否有新版本;「默认模型」写入 CLI 自己的配置文件(CodeBee 之外也生效),CodeBee 编排运行时用哪个供应商 / 模型请到「CLI 绑定」页配置。安装命令不明的条目可直接编辑": "With \"Takes part in orchestration\" enabled, this agent shows up in the task form and review groups. Opening this page auto-checks each CLI for updates; \"default model\" is written into the CLI's own config file (takes effect outside CodeBee too) — to pick which provider/model CodeBee uses at runtime, go to the \"CLI Bindings\" page. Entries with an unknown install command can be edited directly in",
842
+ "启用「参与编排」后,该智能体会出现在任务表单与评审组中。进入本页会自动检查各 CLI 是否有新版本;「默认模型」写入 CLI 自己的配置文件(CodeBee 之外也生效),CodeBee 编排运行时用哪个供应商 / 模型请到「CLI 绑定」页配置。点「一键绑定推荐模型」可为默认模型还空着的智能体自动填入推荐;厂商 / 模型停用或启用后也会自动补绑一次。安装命令不明的条目可直接编辑": "With \"Takes part in orchestration\" enabled, this agent shows up in the task form and review groups. Opening this page auto-checks each CLI for updates; \"default model\" is written into the CLI's own config file (takes effect outside CodeBee too) — to pick which provider/model CodeBee uses at runtime, go to the \"CLI Bindings\" page. \"Auto-bind recommended models\" fills in a recommendation for agents whose default model is empty; rebinding also runs automatically when providers/models are disabled or enabled. Entries with an unknown install command can be edited directly in",
843
843
  "是默认皮肤,即原本的界面。": " is the default skin — the original interface.",
844
844
  "每套皮肤都是一份完整配色,并且各带「日间 / 夜间」两版,明暗切换不会丢皮肤。选中的皮肤立即生效并记在这台设备上(手机与电脑各自独立)。": "Each skin is a complete palette, with day / night variants — switching brightness never loses your skin. The selected skin applies instantly and is remembered on this device (phone and computer are independent).",
845
845
  "系统自己的经验沉淀:": "The system's own distilled experience:",
@@ -869,6 +869,13 @@
869
869
  "模型已停用": "Model disabled",
870
870
  "没有可推荐的:先到「模型接入」页导入与 CLI 协议匹配的供应商。": "Nothing to recommend — import providers matching the CLI protocols on the \"Models\" page first.",
871
871
  "所有 CLI 都已配置模型链,无需推荐。": "All CLIs already have model chains — no recommendation needed.",
872
+ "一键绑定推荐模型": "Auto-bind recommended models",
873
+ "没有支持写入默认模型的已安装智能体。": "No installed agents support writing a default model.",
874
+ "所有已安装智能体都已配置默认模型,无需绑定。": "All installed agents already have a default model — nothing to bind.",
875
+ "已为 %1 个智能体写入推荐模型,%2 个失败。": "Wrote recommended models to %1 agent(s), %2 failed.",
876
+ "已为 %1 个智能体写入推荐模型(原配置已自动备份 .bak)。": "Wrote recommended models to %1 agent(s) (original configs backed up as .bak).",
877
+ "推荐模型写入失败:": "Failed to write recommended models: ",
878
+ "厂商/模型变动,已自动重绑 %1 处。": "Providers/models changed — %1 binding(s) auto-rebound.",
872
879
  "已保存 %1 个 CLI 的绑定。": "Saved bindings for %1 CLI(s).",
873
880
  "未安装": "Not installed",
874
881
  "有新版本 ": "New version ",
@@ -1164,6 +1171,7 @@
1164
1171
  "接管": "Take over",
1165
1172
  "打开网页": "Open web",
1166
1173
  "连接异常": "connection issue",
1174
+ "绑定链失效": "binding chain dead",
1167
1175
  "连续失败": "consecutive failures",
1168
1176
  "一致性": "Consistency",
1169
1177
  "人物塑造": "Characters",
@@ -1193,10 +1201,10 @@
1193
1201
  "),无需升级。": ") — no update needed.",
1194
1202
  "+ 新建自定义流程": "+ New custom flow",
1195
1203
  ",可在「关于与更新」一键升级": " — one-click upgrade in About & Updates",
1196
- ",当前没有可注入的 CLI,编排时会回落为 CLI 默认配置。": ", but no injectable CLI matches it — orchestration falls back to CLI defaults.",
1204
+ ",当前没有可注入的 CLI,编排时相关步骤会直接判失败。": ", but no injectable CLI matches it — affected orchestration steps fail hard.",
1197
1205
  ":连续失败 ": ": consecutive failures ",
1198
1206
  "该供应商协议为 ": "This provider's protocol is ",
1199
- "该供应商已停用:编排时不会注入它,将回落为 CLI 默认配置(模型链也不会生效)。": "This provider is disabled: orchestration won't inject it and will fall back to CLI defaults (the model chain won't apply either).",
1207
+ "该供应商已停用:编排时不会注入它,模型链因此失效时相关步骤会直接判失败。": "This provider is disabled: orchestration won't inject it, and affected steps fail hard when the model chain is invalidated.",
1200
1208
  "绑定后编排调用会注入该供应商的 API key 与地址;不绑定则只按下方模型链传 -m 参数。": "When bound, orchestrated calls inject this provider's API key and URL; otherwise only the model chain below is passed via -m.",
1201
1209
  "还没有可选模型:先到「模型接入」页导入供应商并获取模型列表。": "No models available yet — import a provider and fetch its model list on the \"Models\" page first.",
1202
1210
  "还没有已安装且可编排的 CLI——先到「智能体管理」页安装并启用。": "No installed, orchestration-capable CLI yet — install and enable one on the \"Agents\" page.",
@@ -0,0 +1,79 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
2
+ <!-- CodeBee 写实小蜂:琥珀黑条纹腹 + 绒毛胸背 + 半透明翅脉 + 扇翅动画。
3
+ 经 <img> 引用(独立文档,渐变 id 互不串扰);翅动媒体查询随系统 reduce-motion 关停。 -->
4
+ <defs>
5
+ <linearGradient id="cbAbd" x1="0" y1="0" x2="0" y2="1">
6
+ <stop offset="0" stop-color="#ffcd66"/>
7
+ <stop offset=".55" stop-color="#f0a92c"/>
8
+ <stop offset="1" stop-color="#c67c12"/>
9
+ </linearGradient>
10
+ <radialGradient id="cbThx" cx=".38" cy=".3" r="1.05">
11
+ <stop offset="0" stop-color="#7a5a26"/>
12
+ <stop offset=".6" stop-color="#3d2c10"/>
13
+ <stop offset="1" stop-color="#1f1505"/>
14
+ </radialGradient>
15
+ <linearGradient id="cbWing" x1="0" y1="0" x2="1" y2="1">
16
+ <stop offset="0" stop-color="#ffffff" stop-opacity=".85"/>
17
+ <stop offset="1" stop-color="#cfe0f5" stop-opacity=".16"/>
18
+ </linearGradient>
19
+ <clipPath id="cbAbdClip"><ellipse cx="32" cy="44" rx="11.5" ry="13.5"/></clipPath>
20
+ </defs>
21
+ <style>
22
+ .wingL, .wingR { transform-box: view-box; }
23
+ .wingL { transform-origin: 27px 21px; animation: cbFlapL .16s ease-in-out infinite alternate; }
24
+ .wingR { transform-origin: 37px 21px; animation: cbFlapR .16s ease-in-out infinite alternate; }
25
+ @keyframes cbFlapL { from { transform: rotate(-16deg); } to { transform: rotate(9deg); } }
26
+ @keyframes cbFlapR { from { transform: rotate(16deg); } to { transform: rotate(-9deg); } }
27
+ @media (prefers-reduced-motion: reduce) { .wingL, .wingR { animation: none; } }
28
+ </style>
29
+
30
+ <!-- 触角 -->
31
+ <path d="M29.5 8C27.5 4.6 24.6 3 21.8 3.6" fill="none" stroke="#241a05" stroke-width="1.4" stroke-linecap="round"/>
32
+ <path d="M34.5 8c2-3.4 4.9-5 7.7-4.4" fill="none" stroke="#241a05" stroke-width="1.4" stroke-linecap="round"/>
33
+ <circle cx="21.6" cy="3.7" r="1.5" fill="#241a05"/>
34
+ <circle cx="42.4" cy="3.7" r="1.5" fill="#241a05"/>
35
+
36
+ <!-- 左翅(前后两片,扇翅绕胸侧基点) -->
37
+ <g class="wingL">
38
+ <ellipse cx="16.5" cy="19.5" rx="11.5" ry="4.8" transform="rotate(-34 26 20.5)"
39
+ fill="url(#cbWing)" stroke="#e8f1fb" stroke-opacity=".65" stroke-width=".7"/>
40
+ <ellipse cx="20" cy="24.5" rx="8.2" ry="3.5" transform="rotate(-22 26.5 24.5)"
41
+ fill="url(#cbWing)" stroke="#e8f1fb" stroke-opacity=".55" stroke-width=".6"/>
42
+ <g transform="rotate(-34 26 20.5)" fill="none" stroke="#a9c1dd" stroke-opacity=".4" stroke-width=".5">
43
+ <path d="M26 20.5 8 15.5"/><path d="M26 21.5 7.5 20.5"/>
44
+ </g>
45
+ </g>
46
+ <!-- 右翅 -->
47
+ <g class="wingR">
48
+ <ellipse cx="47.5" cy="19.5" rx="11.5" ry="4.8" transform="rotate(34 38 20.5)"
49
+ fill="url(#cbWing)" stroke="#e8f1fb" stroke-opacity=".65" stroke-width=".7"/>
50
+ <ellipse cx="44" cy="24.5" rx="8.2" ry="3.5" transform="rotate(22 37.5 24.5)"
51
+ fill="url(#cbWing)" stroke="#e8f1fb" stroke-opacity=".55" stroke-width=".6"/>
52
+ <g transform="rotate(34 38 20.5)" fill="none" stroke="#a9c1dd" stroke-opacity=".4" stroke-width=".5">
53
+ <path d="M38 20.5 56 15.5"/><path d="M38 21.5 56.5 20.5"/>
54
+ </g>
55
+ </g>
56
+
57
+ <!-- 腹部:琥珀底 + 黑条纹 + 高光 + 螫针 -->
58
+ <ellipse cx="32" cy="44" rx="11.5" ry="13.5" fill="url(#cbAbd)"/>
59
+ <g clip-path="url(#cbAbdClip)" fill="#241a05">
60
+ <path d="M20 40.5 Q32 36.5 44 40.5 L44 46.5 Q32 42.5 20 46.5 Z"/>
61
+ <path d="M20.5 49 Q32 45.5 43.5 49 L43.2 54.5 Q32 51.5 20.8 54.5 Z" opacity=".92"/>
62
+ </g>
63
+ <path d="M25.2 36q-2.6 4-2 9.5" stroke="#ffe9ad" stroke-opacity=".7" stroke-width="1.6" stroke-linecap="round" fill="none"/>
64
+ <ellipse cx="32" cy="44" rx="11.5" ry="13.5" fill="none" stroke="#8a5a10" stroke-opacity=".35" stroke-width=".8"/>
65
+ <path d="M32 58.5 29.8 54h4.4Z" fill="#241a05"/>
66
+
67
+ <!-- 胸背:绒毛渐变 + 虚线描边模拟绒毛缘 -->
68
+ <ellipse cx="32" cy="25.5" rx="9.2" ry="9.8" fill="url(#cbThx)"/>
69
+ <ellipse cx="32" cy="25.5" rx="9.2" ry="9.8" fill="none" stroke="#8a6a2e" stroke-opacity=".45"
70
+ stroke-width="1" stroke-dasharray="1.2 1.6"/>
71
+
72
+ <!-- 头:复眼 + 高光 -->
73
+ <circle cx="32" cy="12.8" r="6.3" fill="#241a05"/>
74
+ <ellipse cx="28" cy="12.6" rx="2.4" ry="3" fill="#0e0a03"/>
75
+ <ellipse cx="36" cy="12.6" rx="2.4" ry="3" fill="#0e0a03"/>
76
+ <circle cx="28.9" cy="11.3" r=".7" fill="#7d6538" opacity=".8"/>
77
+ <circle cx="36.9" cy="11.3" r=".7" fill="#7d6538" opacity=".8"/>
78
+ <path d="M28.5 8.6q3.5-1.6 7 0" stroke="#8a6a2e" stroke-opacity=".55" stroke-width=".8" fill="none"/>
79
+ </svg>
package/app/ui/index.html CHANGED
@@ -519,11 +519,12 @@
519
519
  <div class="panel-head">
520
520
  <h2 data-i18n="智能体目录">智能体目录</h2>
521
521
  <div class="actions">
522
+ <button data-i18n="一键绑定推荐模型" id="btn-catalog-autobind" class="ghost">一键绑定推荐模型</button>
522
523
  <button data-i18n="重新加载 catalog" id="btn-reload-catalog" class="ghost">重新加载 catalog</button>
523
524
  <button data-i18n="恢复默认 catalog" id="btn-reset-catalog" class="ghost">恢复默认 catalog</button>
524
525
  </div>
525
526
  </div>
526
- <p class="hint"><span data-i18n="启用「参与编排」后,该智能体会出现在任务表单与评审组中。进入本页会自动检查各 CLI 是否有新版本;「默认模型」写入 CLI 自己的配置文件(CodeBee 之外也生效),CodeBee 编排运行时用哪个供应商 / 模型请到「CLI 绑定」页配置。安装命令不明的条目可直接编辑">启用「参与编排」后,该智能体会出现在任务表单与评审组中。进入本页会自动检查各 CLI 是否有新版本;「默认模型」写入 CLI 自己的配置文件(CodeBee 之外也生效),CodeBee 编排运行时用哪个供应商 / 模型请到「CLI 绑定」页配置。安装命令不明的条目可直接编辑</span> <code>data/catalog.json</code> <span data-i18n="后点重新加载。">后点重新加载。</span></p>
527
+ <p class="hint"><span data-i18n="启用「参与编排」后,该智能体会出现在任务表单与评审组中。进入本页会自动检查各 CLI 是否有新版本;「默认模型」写入 CLI 自己的配置文件(CodeBee 之外也生效),CodeBee 编排运行时用哪个供应商 / 模型请到「CLI 绑定」页配置。点「一键绑定推荐模型」可为默认模型还空着的智能体自动填入推荐;厂商 / 模型停用或启用后也会自动补绑一次。安装命令不明的条目可直接编辑">启用「参与编排」后,该智能体会出现在任务表单与评审组中。进入本页会自动检查各 CLI 是否有新版本;「默认模型」写入 CLI 自己的配置文件(CodeBee 之外也生效),CodeBee 编排运行时用哪个供应商 / 模型请到「CLI 绑定」页配置。点「一键绑定推荐模型」可为默认模型还空着的智能体自动填入推荐;厂商 / 模型停用或启用后也会自动补绑一次。安装命令不明的条目可直接编辑</span> <code>data/catalog.json</code> <span data-i18n="后点重新加载。">后点重新加载。</span></p>
527
528
  <h3 data-i18n="已安装" class="sec-title">已安装</h3>
528
529
  <div id="ag-installed" class="cards"></div>
529
530
  <h3 data-i18n="可安装" class="sec-title">可安装</h3>