codebee 0.1.19 → 0.1.21

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
@@ -13,6 +13,12 @@ async function loadFlows() {
13
13
  renderTypeOptions();
14
14
  }
15
15
 
16
+ function invalidateFlowRubricDraft(flowId) {
17
+ if (S.flowRubricDrafts) delete S.flowRubricDrafts[flowId];
18
+ const rubric = $("f-rubric");
19
+ if (rubric && rubric.dataset.flowId === flowId) rubric.dataset.flowId = "";
20
+ }
21
+
16
22
  function flowById(id) {
17
23
  return (S.flows || []).find((f) => f.id === id) || null;
18
24
  }
@@ -78,7 +84,7 @@ function renderTypeMenu() {
78
84
  * 拉到手后就地更新菜单项——菜单开着也不重建 DOM,不打断滚动/悬停 */
79
85
  async function fetchFlowEstimates() {
80
86
  if (!S.flows) return;
81
- const targets = S.flows.slice(0, 10);
87
+ const targets = S.flows;
82
88
  const results = await Promise.all(targets.map(async (f) => {
83
89
  try {
84
90
  const d = await api("/api/usage/estimate?type=" + encodeURIComponent(f.id) + "&days=90");
@@ -157,8 +163,18 @@ function onTypeChange() {
157
163
  if (flow.rounds) $("f-rounds").value = flow.rounds;
158
164
  if (flow.threshold) $("f-threshold").value = flow.threshold;
159
165
  if (flow.best_of) $("f-bestof").value = flow.best_of;
160
- const saved = ($("f-rubric").value || "").trim();
161
- if (!saved && flow.rubric) $("f-rubric").value = flow.rubric.join(", ");
166
+ const rubric = $("f-rubric");
167
+ const previousFlow = rubric.dataset.flowId || "";
168
+ if (!S.flowRubricDrafts) S.flowRubricDrafts = {};
169
+ if (previousFlow && previousFlow !== flow.id) {
170
+ S.flowRubricDrafts[previousFlow] = rubric.value;
171
+ }
172
+ if (previousFlow !== flow.id) {
173
+ rubric.value = Object.prototype.hasOwnProperty.call(S.flowRubricDrafts, flow.id)
174
+ ? S.flowRubricDrafts[flow.id]
175
+ : (flow.rubric || []).join(", ");
176
+ rubric.dataset.flowId = flow.id;
177
+ }
162
178
  // 连载参数预填(用户可改/可清空 = 单稿件模式)
163
179
  if (flow.serial) {
164
180
  if (!$("f-chapters").value) $("f-chapters").value = flow.serial.chapters || "";
@@ -410,13 +426,13 @@ function jsq(s) {
410
426
  }
411
427
 
412
428
  function statusChip(st) {
413
- const zh = { queued: t("排队中"), running: t("运行中"), done: t("完成"), failed: t("失败"), cancelled: t("已取消"), timeout: t("超时") };
429
+ const zh = { queued: t("正在启动"), running: t("运行中"), done: t("完成"), failed: t("失败"), cancelled: t("已取消"), timeout: t("超时") };
414
430
  return '<span class="chip ' + esc(st) + '">' + (zh[st] || esc(st)) + "</span>";
415
431
  }
416
432
 
417
433
  /* 运行状态文案(传 run 对象):退避窗口内的续跑副本写明「将于 HH:MM 自动
418
434
  * 续跑」,别让 5 分钟等待看起来像卡死/资源排队(2026-09-18 重写任务误判案)。
419
- * 到点后翻回「排队中」——页面轮询重渲染时 Date.now() 已过预定时刻。 */
435
+ * 到点后翻回「正在启动」——页面轮询重渲染时 Date.now() 已过预定时刻。 */
420
436
  function runStatusText(run) {
421
437
  const st = String((run && run.status) || "");
422
438
  if (st === "queued" && run && run.resume_enqueue_at) {
@@ -424,7 +440,7 @@ function runStatusText(run) {
424
440
  if (!isNaN(at) && Date.now() < at)
425
441
  return t("将于 {0} 自动续跑", String(run.resume_enqueue_at).slice(11, 16));
426
442
  }
427
- return { queued: t("排队中"), running: t("运行中"), done: t("完成"),
443
+ return { queued: t("正在启动"), running: t("运行中"), done: t("完成"),
428
444
  failed: t("失败"), cancelled: t("已取消"), timeout: t("超时") }[st] || st;
429
445
  }
430
446
 
@@ -616,7 +632,7 @@ async function healthOp(op) {
616
632
  async function healthDisableModel(pid, model) {
617
633
  if (!pid || !model) { closeModal(); return; }
618
634
  const yes = await uiConfirm(
619
- t("确认禁用模型 {0}?链降级将自动跳过它,其余模型不受影响;可在 CLI 绑定页重新启用。").replace("{0}", pid + " · " + model),
635
+ t("确认禁用模型 {0}?链降级将自动跳过它,其余模型不受影响;可在模型调度页重新启用。").replace("{0}", pid + " · " + model),
620
636
  { title: t("禁用模型"), danger: true, ok: t("禁用") });
621
637
  if (!yes) return;
622
638
  try {
@@ -641,7 +657,7 @@ async function healthDisableModel(pid, model) {
641
657
  async function healthDisableProvider(pid) {
642
658
  if (!pid) { closeModal(); return; }
643
659
  const yes = await uiConfirm(
644
- t("确认禁用该厂商?禁用后链降级自动跳过它,恢复后可在 CLI 绑定页重新启用。"),
660
+ t("确认禁用该厂商?禁用后链降级自动跳过它,恢复后可在模型调度页重新启用。"),
645
661
  { title: t("禁用厂商"), danger: true, ok: t("禁用") });
646
662
  if (!yes) return;
647
663
  try {
@@ -839,7 +855,7 @@ function renderBindings() {
839
855
  }
840
856
  bbox.innerHTML = targets.map((c) => {
841
857
  const b = (S.bindings || {})[c.id] || {};
842
- const opts = '<option value="">' + t("不绑定(用 CLI 自身的凭据与配置)") + '</option>' + bindable.map((p) => {
858
+ const opts = '<option value="">' + t("自动推荐(推荐)") + '</option>' + bindable.map((p) => {
843
859
  const adaptedOnly = p.protocol !== "anthropic" && p.protocol !== "openai";
844
860
  return '<option value="' + esc(p.id) + '"' + (b.provider_id === p.id ? " selected" : "") + ">" +
845
861
  esc(p.name) + t("(") + esc(protoLabel(p)) + (adaptedOnly ? t(" · 已适配") : "") +
@@ -863,13 +879,13 @@ function renderBindings() {
863
879
  return '<div class="card"><div class="head"><span class="name">' + esc(c.name) + "</span>" +
864
880
  '<span class="tag">' + esc(c.orch_kind) + "</span></div>" +
865
881
  '<div class="field"><label>' + t("供应商") + '</label><select id="bindprov-' + esc(c.id) + '">' + opts + "</select></div>" +
866
- '<p class="hint">' + t("绑定后编排调用会注入该供应商的 API key 与地址;不绑定则只按下方模型链传 -m 参数。") + '</p>' +
882
+ '<p class="hint">' + t("默认不需要绑定:系统会按任务类型、难度、成本与健康状态自动推荐可用模型;只有需要固定厂商、模型或降级顺序时才在这里绑定。绑定后编排调用会注入该供应商的 API key 与地址;完全未指定时保持自动调度,没有兼容可用供应商才使用 CLI 自身配置。") + '</p>' +
867
883
  bindModelBox(c, b.provider_id) + offWarn + protoWarn + chainWarn +
868
884
  '<div class="ops"><label class="toggle"><input type="checkbox" id="binddiff-' + esc(c.id) + '"' +
869
885
  (b.difficulty_routing ? " checked" : "") + '>' + t(" 按难度自动选模型(简单/困难)") + '</label>' +
870
886
  '<button class="ghost small" onclick="saveBinding(\'' + esc(c.id) + '\')">' + t("保存") + '</button></div></div>';
871
887
  }).join("") +
872
- (!targets.length ? '<p class="hint">' + t("还没有已安装且可编排的 CLI——先到「智能体管理」页安装并启用。") + '</p>' : "") +
888
+ (!targets.length ? '<p class="hint">' + t("还没有已安装且可编排的 CLI——先到「本机智能体」页安装并启用。") + '</p>' : "") +
873
889
  (nonBindable ? '<p class="hint">' + t("另有 ") + nonBindable +
874
890
  t(" 个供应商(google 等协议)仅登记,不支持注入 CLI,未出现在上面的下拉中。") + '</p>' : "");
875
891
  }
@@ -958,7 +974,7 @@ async function batchProvOp(op) {
958
974
  const tips = {
959
975
  enable: t("启用所选 ") + ids.length + t(" 个供应商?"),
960
976
  disable: t("停用所选 ") + ids.length + t(" 个供应商?\n停用后其绑定会回落为 CLI 默认;配置与模型列表都保留,可随时再启用。"),
961
- delete: t("删除所选 ") + ids.length + t(" 个供应商?\n相关 CLI 绑定会自动解绑,此操作不可撤销。"),
977
+ delete: t("删除所选 ") + ids.length + t(" 个供应商?\n相关显式模型调度会自动解除,此操作不可撤销。"),
962
978
  };
963
979
  if (!await uiConfirm(tips[op] || (t("执行「") + op + t("」?")))) return;
964
980
  try {
@@ -1764,11 +1780,15 @@ async function doImport() {
1764
1780
 
1765
1781
  async function saveBinding(id) {
1766
1782
  const st = bindSelById(id);
1783
+ const provSel = $("bindprov-" + id);
1767
1784
  // 主供应商跟链首走:模型链是唯一真源,链首非空时供应商下拉必须与之一致
1768
1785
  // (下拉是旧状态时把旧值发上去,后端「显式指定」就会盖回停用的旧供应商)。
1769
- const headP = st.chain.length ? st.chain[0].p : "";
1770
- const provSel = $("bindprov-" + id);
1771
- if (provSel && provSel.value !== (headP || "")) provSel.value = headP || "";
1786
+ // 空链时保留下拉选择:这是“只锁定供应商,模型按该供应商默认/难度选”的
1787
+ // 合法显式覆盖,不能重置成自动模式。
1788
+ const headP = st.chain.length ? st.chain[0].p : (provSel ? provSel.value : "");
1789
+ if (st.chain.length && provSel && provSel.value !== (headP || "")) {
1790
+ provSel.value = headP || "";
1791
+ }
1772
1792
  try {
1773
1793
  await api("/api/models/binding", { method: "POST", body: JSON.stringify({
1774
1794
  agent_id: id, provider_id: provSel ? provSel.value : "",
@@ -1910,7 +1930,8 @@ async function createTask() {
1910
1930
  msg.textContent = t("目标有点简短,先问几个问题…");
1911
1931
  try {
1912
1932
  const cq = await api("/api/tasks/clarify", {
1913
- method: "POST", body: JSON.stringify({ goal: payload.goal, type: payload.type }) });
1933
+ method: "POST", timeout: 75000,
1934
+ body: JSON.stringify({ goal: payload.goal, type: payload.type }) });
1914
1935
  const qs = (cq && cq.questions) || [];
1915
1936
  if (qs.length) {
1916
1937
  S.clarifyDone = true; // 本轮已采访;再点发送直接创建
@@ -1968,7 +1989,8 @@ async function createTask() {
1968
1989
  if (critics.length) payload.critics = critics;
1969
1990
  }
1970
1991
  try {
1971
- const r = await api("/api/tasks", { method: "POST", body: JSON.stringify(payload) });
1992
+ const r = await api("/api/tasks", { method: "POST", timeout: 30000,
1993
+ body: JSON.stringify(payload) });
1972
1994
  msg.textContent = t("已创建,跳转运行页…");
1973
1995
  // 先把新任务刷进 state 再跳:chatEngineIsDirect 靠 S.state.tasks 判引擎,
1974
1996
  // 不刷的话对话页签不会就绪,自动选卡落不到「对话」
@@ -3244,7 +3266,7 @@ function drawTaskDetail(key, runs) {
3244
3266
  // 取消收尾把僵尸步骤落成「已取消」时要立即重画,不等条数变化;
3245
3267
  // 作品信息状态入签名:后台一键生成 running→done 要立刻反映到成果区面板
3246
3268
  const bmTask = ((S.state || {}).tasks || []).find((x) => x.id === key);
3247
- // 自动续跑退避相位入签名:预定入队时刻过了之后 chip 要从「将于 HH:MM」翻回「排队中」
3269
+ // 自动续跑退避相位入签名:预定启动时刻过了之后 chip 翻为「正在启动」
3248
3270
  const lr0 = runs[0] || {};
3249
3271
  const resumePending = (lr0.resume_enqueue_at &&
3250
3272
  Date.now() < Date.parse(String(lr0.resume_enqueue_at).replace(" ", "T"))) ? 1 : 0;
@@ -3258,8 +3280,8 @@ function drawTaskDetail(key, runs) {
3258
3280
  const ordered = runs.slice(); // 详情按最近运行优先,方便排查
3259
3281
  const totalSteps = runs.reduce((a, r) => a + (r.steps || []).length, 0);
3260
3282
  const active = runs.some((r) => r.status === "running" || r.status === "queued");
3261
- // 活跃态细分真实状态:排队里还分「等并发」和「自动续跑退避(预定 HH:MM 入队)」,
3262
- // 后者在 chip 上写明下一轮何时起跑,别让 5 分钟退避窗口看起来像卡死(Z.ai 误伤案)
3283
+ // 活跃态细分真实状态:queued 仅用于自动续跑退避或创建到起跑的瞬时状态;
3284
+ // 前者在 chip 上写明下一轮何时起跑,避免退避窗口看起来像卡死。
3263
3285
  const activeRun0 = runs.find((r) => r.status === "running" || r.status === "queued");
3264
3286
  const st = activeRun0 ? activeRun0.status : latest.status;
3265
3287
  const resumeIn = (st === "queued" && resumePending)
@@ -3269,7 +3291,7 @@ function drawTaskDetail(key, runs) {
3269
3291
  chip.className = "chip " + st;
3270
3292
  chip.textContent = resumeIn
3271
3293
  ? t("将于 ") + resumeIn + t(" 自动续跑(第 ") + (Number(latest.auto_resumes) || 0) + t(" 次)")
3272
- : ({ queued: t("排队中"), running: t("运行中"), done: t("完成"), failed: t("失败"), cancelled: t("已取消") }[st] || st);
3294
+ : ({ queued: t("正在启动"), running: t("运行中"), done: t("完成"), failed: t("失败"), cancelled: t("已取消") }[st] || st);
3273
3295
  const bpt = $("btn-pause");
3274
3296
  if (bpt) bpt.classList.add("hidden");
3275
3297
  $("btn-delete").classList.add("hidden");
@@ -6041,12 +6063,17 @@ function chatResultHTML(run, res) {
6041
6063
  if (res.turns) meta.push(res.turns + " " + t("轮对话"));
6042
6064
  if (res.duration_s != null) meta.push(chatDurTxt(res.duration_s));
6043
6065
  const files = res.files || [];
6044
- const chips = files.map((f) =>
6045
- '<a class="file-chip chat-file-open" href="' + urlAuth("/api/runs/" + encodeURIComponent(run.id) + "/file?name=" +
6046
- encodeURIComponent(f.name)) + '" target="_blank" rel="noopener" title="' +
6047
- 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) + '">' +
6048
- '<i class="fx">' + esc(_fpExt(f.name).slice(0, 4) || "file") + "</i>" +
6049
- '<span class="p">' + esc(f.name) + "</span><i>" + fmtSize(f.size) + "</i></a>").join("");
6066
+ const chips = files.map((f) => {
6067
+ const fUrl = urlAuth("/api/runs/" + encodeURIComponent(run.id) + "/file?name=" + encodeURIComponent(f.name));
6068
+ return '<div class="file-chip has-actions">' +
6069
+ '<i class="fx">' + esc(_fpExt(f.name).slice(0, 4) || "file") + "</i>" +
6070
+ '<span class="p" title="' + esc(f.name + " · " + fmtSize(f.size)) + '">' + esc(f.name) + "</span>" +
6071
+ '<span class="fsz">' + fmtSize(f.size) + "</span>" +
6072
+ '<span class="fbtns">' +
6073
+ '<button type="button" class="chip-btn" title="' + esc(t("预览")) + '" onclick="artPopup(\'' + esc(run.id) + "', '" + esc(f.name) + "', " + (Number(f.size) || 0) + ')"><svg class="ico" aria-hidden="true"><use href="#i-file-text"/></svg></button>' +
6074
+ '<a class="chip-btn" href="' + fUrl + '" download="' + esc(f.name) + '" title="' + esc(t("下载")) + '"><svg class="ico" aria-hidden="true"><use href="#i-arrow-left"/></svg></a>' +
6075
+ "</span></div>";
6076
+ }).join("");
6050
6077
  return '<div class="chat-row">' +
6051
6078
  '<span class="chat-avatar" aria-hidden="true"><svg class="ico"><use href="' + icon + '"></use></svg></span>' +
6052
6079
  '<div class="chat-result' + (ok ? "" : bad ? " bad" : " off") + '">' +
@@ -6861,12 +6888,11 @@ function chainDeadReasons(c, chain) {
6861
6888
  return dead;
6862
6889
  }
6863
6890
 
6864
- /* 单条绑定链的推荐修复动作:需要改返回新链,不动返回 null。
6865
- * 空链 → 预填推荐;整条死透(或只有链首且已死)→ 重推荐;链首死但链内还有
6866
- * 活的备选 新链首插最前(原降级序保留,推荐项已在链内则升首去重)。 */
6867
- function bindRepairAction(c, st) {
6891
+ /* 单条显式绑定链的推荐修复动作:需要改返回新链,不动返回 null。
6892
+ * 空链代表默认自动调度,不能悄悄写成持久绑定;已有链失效时才推荐替代链。 */
6893
+ function bindRepairAction(c, st, includeEmpty) {
6868
6894
  const rec = recommendFor(c);
6869
- if (!st.chain.length) return rec ? [rec] : null;
6895
+ if (!st.chain.length) return includeEmpty && rec ? [rec] : null;
6870
6896
  const dead = chainDeadReasons(c, st.chain);
6871
6897
  const allDead = dead.every((d) => d);
6872
6898
  const headDead = dead[0] !== ""; // 空 p = CLI 默认凭据,是有意配置不算死
@@ -6881,7 +6907,7 @@ function autoBindAll() {
6881
6907
  let filled = 0, skipped = 0, noProv = 0, refilled = 0;
6882
6908
  for (const c of targets) {
6883
6909
  const st = bindSelById(c.id);
6884
- const act = bindRepairAction(c, st);
6910
+ const act = bindRepairAction(c, st, true);
6885
6911
  if (!act) {
6886
6912
  // 没动它:分清「健康链无需推荐」和「没有可推荐的」两种落空
6887
6913
  if (!st.chain.length) { noProv++; continue; }
@@ -6910,9 +6936,8 @@ function autoBindAll() {
6910
6936
  }
6911
6937
  }
6912
6938
 
6913
- /* 厂商/模型停用·启用·删除后自动补一次推荐绑定(2026-09-17 用户拍板):
6914
- * 绑定链——空链预填、死链重推荐、死链首插新首,与「一键推荐绑定」同规则,
6915
- * 但直接落盘(自动场景没有人工确认环节);目录页——空默认模型直填。
6939
+ /* 厂商/模型停用·启用·删除后自动修复显式绑定:
6940
+ * 空链保持自动调度,不写入绑定;已有链失效时才直接落盘替代链。
6916
6941
  * 没有合适的推荐就保持原样,什么都不绑。绑定页上用户手改中的草稿(dirty)
6917
6942
  * 不碰;一处都没改成静默返回,不打扰停用/启用的操作反馈。 */
6918
6943
  let _autoRebindRunning = false, _autoRebindAgain = false;
@@ -6925,7 +6950,7 @@ async function autoRebindSoon() {
6925
6950
  for (const c of (S.catalog || []).filter((x) => x.installed && x.orch_kind)) {
6926
6951
  const st = bindSelById(c.id);
6927
6952
  if (st.dirty) continue; // 用户手改中,不覆盖草稿
6928
- const act = bindRepairAction(c, st);
6953
+ const act = bindRepairAction(c, st, false);
6929
6954
  if (!act) continue;
6930
6955
  const b = (S.bindings || {})[c.id] || {};
6931
6956
  try {
@@ -6937,17 +6962,6 @@ async function autoRebindSoon() {
6937
6962
  fixed++;
6938
6963
  } catch (e) { /* 单条失败不打断,等下次变更再补 */ }
6939
6964
  }
6940
- for (const c of (S.catalog || []).filter((x) =>
6941
- x.installed && x.config_writable && !fmtModel(x.model))) {
6942
- const rec = recommendFor(c);
6943
- if (!rec) continue;
6944
- try {
6945
- const r = await api("/api/catalog/" + encodeURIComponent(c.id) + "/model",
6946
- { method: "POST", body: JSON.stringify({ model: rec.m }) });
6947
- c.model = r.model || rec.m;
6948
- fixed++;
6949
- } catch (e) { /* 同上 */ }
6950
- }
6951
6965
  if (fixed) {
6952
6966
  S.catSig = null; S.bindSig = null;
6953
6967
  render();
@@ -7008,7 +7022,7 @@ function bindModelBox(c, provId) {
7008
7022
  }).join("")
7009
7023
  : '<span class="hint">' + t("未设置") + (provId
7010
7024
  ? t("(按供应商/难度自动解析——供应商协议不匹配或被停用时解析为空,相关步骤将判失败)")
7011
- : t("(用 CLI 默认模型——不会注入任何供应商凭据)")) + "</span>";
7025
+ : t("(自动推荐厂商与模型;没有兼容可用供应商时才使用 CLI 自身配置)")) + "</span>";
7012
7026
  return '<div class="field"><label>' + t("运行时模型链(跨厂商,最多 ") + MAX_ORCH_MODELS + t(" 条)") + "</label>" +
7013
7027
  '<div class="orch-row">' + chips +
7014
7028
  '<button class="ghost small" onclick="bindToggle(\'' + esc(c.id) + '\')">' +
@@ -7417,7 +7431,8 @@ function openFlowsManager() {
7417
7431
  const rows = flows.map((f) =>
7418
7432
  '<div class="item"><div class="t"><span class="name">' + flowIconHtml(f) + " " + esc(t(f.name)) +
7419
7433
  '</span><span class="tag">' + esc(f.id) + "</span>" +
7420
- '<span class="tag">' + (f.engine === "code" ? t("代码引擎") : t("评审引擎")) + "</span>" +
7434
+ '<span class="tag">' + (f.engine === "code" ? t("代码引擎")
7435
+ : f.engine === "direct" ? t("直连引擎") : t("评审引擎")) + "</span>" +
7421
7436
  (f.serial ? '<span class="tag">' + t("连载 ") + f.serial.chapters + t(" 章") + "</span>" : "") +
7422
7437
  (f.builtin ? '<span class="tag ok">' + t("预置") + '</span>' : '<span class="tag">' + t("自定义") + '</span>') +
7423
7438
  (f.edited ? '<span class="tag">' + t("已改") + '</span>' : "") +
@@ -7440,6 +7455,7 @@ async function flowReset(fid) {
7440
7455
  if (!await uiConfirm(t("把「") + fid + t("」恢复为内置默认配置?"), { ok: t("恢复") })) return;
7441
7456
  try { await api("/api/flows/" + encodeURIComponent(fid) + "/reset", { method: "POST" }); }
7442
7457
  catch (e) { toast(t("恢复失败:") + e.message, true); return; }
7458
+ invalidateFlowRubricDraft(fid);
7443
7459
  await loadFlows();
7444
7460
  openFlowsManager();
7445
7461
  toast(t("已恢复默认"));
@@ -7456,7 +7472,8 @@ function flowForm(fid) {
7456
7472
  '<div class="grid-2">' +
7457
7473
  '<div class="field"><label>' + t("名称 ") + '<span class="req">*</span></label><input id="fl-name" value="' +
7458
7474
  esc(f ? f.name : "") + '" placeholder="' + t("例:播客脚本") + '"></div>' +
7459
- '<div class="field"><label>' + t("引擎 ") + '<span class="req">*</span></label><select id="fl-engine"' +
7475
+ '<div class="field"><label>' + t("引擎 ") + '<span class="req">*</span></label><select id="fl-engine" data-fid="' +
7476
+ esc(f ? f.id : "") + '"' +
7460
7477
  (engLocked ? " disabled" : "") + '>' +
7461
7478
  '<option value="review"' + (engine === "review" ? " selected" : "") + '>' + t("评审引擎(起草 → 多维评审 → 修订 → 门禁)") + '</option>' +
7462
7479
  '<option value="code"' + (engine === "code" ? " selected" : "") + '>' + t("代码引擎(实现 → 验证 → 评审 → 修复)") + '</option>' +
@@ -7530,6 +7547,7 @@ async function saveFlow() {
7530
7547
  await api("/api/flows", { method: "POST", body: JSON.stringify(payload) });
7531
7548
  } catch (e) { toast(t("保存失败:") + e.message, true); return; }
7532
7549
  closeModal();
7550
+ invalidateFlowRubricDraft(id);
7533
7551
  await loadFlows();
7534
7552
  openFlowsManager();
7535
7553
  toast(t("流程已保存"));
@@ -7807,11 +7825,11 @@ function autoTplPace(tp) {
7807
7825
  });
7808
7826
  }
7809
7827
 
7810
- /* last_status → 徽章:queued=正常灰、error=红「拉起失败」、missed=黄「已错过」 */
7828
+ /* last_status → 徽章:started=正常灰、error=红「拉起失败」、missed=黄「已错过」 */
7811
7829
  function autoStatusTag(tsk) {
7812
7830
  if (tsk.last_status === "error") return '<span class="tag auto-tag-err">' + t("拉起失败") + "</span>";
7813
7831
  if (tsk.last_status === "missed") return '<span class="tag auto-tag-miss">' + t("已错过") + "</span>";
7814
- if (tsk.last_status === "queued") return '<span class="tag">' + t("正常") + "</span>";
7832
+ if (tsk.last_status === "started" || tsk.last_status === "queued") return '<span class="tag">' + t("正常") + "</span>";
7815
7833
  return "";
7816
7834
  }
7817
7835
 
@@ -8470,7 +8488,11 @@ function mkrDebounce() {
8470
8488
 
8471
8489
  /* 拉一页:reset=清空已累积列表(首进/搜索/换来源/刷新后)。 */
8472
8490
  async function mkrLoadPage(reset) {
8473
- if (S.mkrLoading) return;
8491
+ // 翻页保持单飞;搜索/换来源/刷新属于重置请求,可以抢占旧请求。
8492
+ // 旧响应回来时由 requestKey 丢弃,不能覆盖用户刚选的新条件。
8493
+ if (S.mkrLoading && !reset) return;
8494
+ const requestKey = (S.mkrRequestKey || 0) + 1;
8495
+ S.mkrRequestKey = requestKey;
8474
8496
  if (reset) { S.mkrAll = []; S.mkrOffset = 0; }
8475
8497
  const q = (($("mkr-search") || {}).value || "").trim();
8476
8498
  const srcSel = $("mkr-source");
@@ -8480,6 +8502,7 @@ async function mkrLoadPage(reset) {
8480
8502
  S.mkrLoading = true;
8481
8503
  let data = null;
8482
8504
  try { data = await api(url); } catch (e) { data = null; }
8505
+ if (requestKey !== S.mkrRequestKey) return;
8483
8506
  S.mkrLoading = false;
8484
8507
  if (!data) { S.marketRemote = null; S.mkrAll = null; renderMarketRemote(); return; }
8485
8508
  S.marketRemote = data;
@@ -8567,7 +8590,8 @@ async function mkrRefresh() {
8567
8590
  const old = btn.textContent;
8568
8591
  btn.textContent = t("拉取中…");
8569
8592
  try {
8570
- const r = await api("/api/market/remote/refresh", { method: "POST", body: "{}" });
8593
+ const r = await api("/api/market/remote/refresh", {
8594
+ method: "POST", timeout: 120000, body: "{}" });
8571
8595
  const bad = (r.refresh || []).filter((x) => !x.ok);
8572
8596
  if (bad.length) toast(t("部分来源拉取失败:") + bad.map((x) => x.error || x.id).join(t(";")), true);
8573
8597
  else toast(t("拉取成功"));
@@ -8900,13 +8924,13 @@ function helpChapterBody(id) {
8900
8924
  if (id === "quickstart") {
8901
8925
  return '<div class="welcome-steps">' +
8902
8926
  '<div class="wstep"><b>1</b><span>' + t("添加模型供应商:设置 → 模型接入,填入 API Key") + '</span></div>' +
8903
- '<div class="wstep"><b>2</b><span>' + t("绑定 CLI 智能体:点「一键推荐绑定」自动配好") + '</span></div>' +
8927
+ '<div class="wstep"><b>2</b><span>' + t("启用本机智能体:运行时默认自动推荐模型") + '</span></div>' +
8904
8928
  '<div class="wstep"><b>3</b><span>' + t("新建任务:选工作目录、写目标,蜂群开工") + '</span></div>' +
8905
8929
  '</div>' +
8906
8930
  '<p class="help-note">' + t("任务跑起来后,详情页能看到步骤、蜂巢、成果文件与 Git 版本;「直接执行」类任务还能像聊天一样边跑边追加消息。") + '</p>' +
8907
8931
  '<div class="welcome-acts">' +
8908
8932
  '<button class="wl-btn primary" onclick="welcomeGo(\'models\')"><span>' + t("开始配置模型") + '</span><svg class="ico" aria-hidden="true"><use href="#i-chevron-r"></use></svg></button>' +
8909
- '<button class="wl-btn" onclick="welcomeGo(\'bindings\')"><span>' + t("绑定 CLI 智能体") + '</span><svg class="ico" aria-hidden="true"><use href="#i-chevron-r"></use></svg></button>' +
8933
+ '<button class="wl-btn" onclick="welcomeGo(\'bindings\')"><span>' + t("模型调度(可选)") + '</span><svg class="ico" aria-hidden="true"><use href="#i-chevron-r"></use></svg></button>' +
8910
8934
  '<button class="wl-btn ghost" onclick="welcomeClose()"><span>' + t("先跳过,直接体验") + '</span></button>' +
8911
8935
  '</div>';
8912
8936
  }
@@ -8915,8 +8939,8 @@ function helpChapterBody(id) {
8915
8939
  '<p class="help-p">' + t("「设置 → 模型接入 → 添加供应商」:填名称、API Key、接口地址(一般用默认)。保存后点「获取模型列表」自动拉取该厂商的模型;网关不提供列表接口时直接手工填模型名即可。同一厂商可配多把 Key,按顺序轮换,某把欠费自动切下一把。") + '</p>' +
8916
8940
  '<h3 class="help-h3">' + t("第二步:认识协议徽章") + '</h3>' +
8917
8941
  '<p class="help-p">' + t("新供应商默认自动识别协议,保存后在后台实测支持哪些调用形态,结果以徽章标在卡片上。带「· chat」表示该模型只适合站内直连对话;codex 这类只讲 responses 协议的 CLI 用不了它,绑定页会自动剔除,不用自己排查。") + '</p>' +
8918
- '<h3 class="help-h3">' + t("第三步:绑定 CLI 智能体") + '</h3>' +
8919
- '<p class="help-p">' + t("「设置CLI 绑定」给每个智能体配一条模型链,运行时按顺序故障转移。「一键推荐绑定」只填空缺,不覆盖手工配置;删除或停用厂商后链会自动补绑。") + '</p>' +
8942
+ '<h3 class="help-h3">' + t("第三步:启用本机智能体") + '</h3>' +
8943
+ '<p class="help-p">' + t("无需预先绑定模型。CodeBee 默认按任务类型、难度、成本和健康状态自动推荐;需要固定厂商、模型或降级顺序时,再到「设置 模型调度(可选)」指定。") + '</p>' +
8920
8944
  '<h3 class="help-h3">' + t("报错速查") + '</h3>' +
8921
8945
  '<ul class="help-list">' +
8922
8946
  '<li><b>401</b><span>' + t("Key 无效或过期——检查或更换 Key。") + '</span></li>' +
@@ -8981,7 +9005,7 @@ function helpChapterBody(id) {
8981
9005
  return '<div class="help-qa"><b>' + t("点「获取模型列表」报 404?") + '</b><p>' + t("部分网关不提供模型列表接口,属正常现象;只要能正常对话就不用管,模型名手工填即可。") + '</p></div>' +
8982
9006
  '<div class="help-qa"><b>' + t("报 503 / 无可用渠道?") + '</b><p>' + t("该模型当前没有可用渠道,最常见是余额耗尽。查一下余额或换个模型;同一厂商配了多把 Key 会自动轮换。") + '</p></div>' +
8983
9007
  '<div class="help-qa"><b>' + t("「冷却中」是什么意思?") + '</b><p>' + t("一把 Key 连续失败会被暂停 30 分钟,防止反复撞墙烧钱,到期自动恢复;也可以在密钥旁手动重置。") + '</p></div>' +
8984
- '<div class="help-qa"><b>' + t("任务一直显示「排队」?") + '</b><p>' + t("本地默认最多 6 个任务同时跑,排满就排队。排队时可以打开详情看前面还有几个;个别卡死的任务,看门狗会自动清理补队。") + '</p></div>' +
9008
+ '<div class="help-qa"><b>' + t("任务一直显示「排队」?") + '</b><p>' + t("新任务默认立即运行,不会排队;达到并发保护上限时本次会直接失败并提示稍后重试。只有自动续跑退避会显示预定时间;历史版本遗留的排队记录会由恢复机制立即接管或收口。") + '</p></div>' +
8985
9009
  '<div class="help-qa"><b>' + t("状态里写着「将于 HH:MM 自动续跑」?") + '</b><p>' + t("这一步失败了,正在退避等待自动重试,到点会接着跑,不需要手动干预;等不及也可以在详情页手动重试。") + '</p></div>' +
8986
9010
  '<div class="help-qa"><b>' + t("生成的文件在哪?") + '</b><p>' + t("写作类任务的章节、封面、报告都落在任务的运行目录,详情页「成果」页签可浏览和预览。代码类任务则在仓库的任务分支上改代码,详情页「版本」里审阅后再合并。") + '</p></div>' +
8987
9011
  '<div class="help-qa"><b>' + t("忘了令牌 / 手机打不开页面?") + '</b><p>' + t("服务启动日志里有带令牌的完整访问地址;远程设备必须用带令牌的 URL 打开(或在令牌门里输入一次),否则会一直要求授权。") + '</p></div>' +
@@ -9284,7 +9308,7 @@ async function suStartupCheck() {
9284
9308
  }
9285
9309
 
9286
9310
  /* ---------------------------------------------------------- 页签 & 初始化 */
9287
- const TAB_TITLES = { tasks: "任务", runs: "运行记录", automation: "自动化", zentao: "禅道 Bug 自动修复", usage: "用量统计", agents: "智能体管理", models: "模型接入", bindings: "CLI 绑定", skills: "经验库", knowledge: "知识库", market: "插件市场", orch: "编排设置", data: "数据与备份", appearance: "皮肤", about: "关于与更新" };
9311
+ const TAB_TITLES = { tasks: "任务", runs: "运行记录", automation: "自动化", zentao: "禅道 Bug 自动修复", usage: "用量统计", agents: "本机智能体", models: "模型接入", bindings: "模型调度(可选)", skills: "经验库", knowledge: "知识库", market: "插件市场", orch: "编排设置", data: "数据与备份", appearance: "皮肤", about: "关于与更新" };
9288
9312
  const SET_TABS = new Set(Object.keys(TAB_TITLES)); // 设置导航里的子页(__phone 是弹框,不算)
9289
9313
 
9290
9314
  function tabTitle(name) {
@@ -10042,7 +10066,7 @@ function renderBackupPreview(pv) {
10042
10066
  rows.push('<span class="bad">' + esc(t("以下外部目录不在备份里,需自行拷贝:"))
10043
10067
  + esc(pv.external_workdirs.join("、")) + "</span>");
10044
10068
  if ((pv.busy_runs || []).length)
10045
- rows.push('<span class="bad">' + esc(t("有任务正在运行或排队,先等它们结束再导入")) + "</span>");
10069
+ rows.push('<span class="bad">' + esc(t("有任务正在运行或等待自动续跑,先等它们结束再导入")) + "</span>");
10046
10070
  el.innerHTML = rows.map((s) => "<div>" + s + "</div>").join("");
10047
10071
  if (!(pv.busy_runs || []).length) $("bi-apply").classList.remove("hidden");
10048
10072
  }
@@ -10726,7 +10750,7 @@ function cmdkOps() {
10726
10750
  { icon: "i-blocks", label: t("插件市场"), run: () => switchTab("market") },
10727
10751
  { icon: "i-book", label: t("经验库"), run: () => switchTab("skills") },
10728
10752
  { icon: "i-sigma", label: t("知识库"), run: () => switchTab("knowledge") },
10729
- { icon: "i-cpu", label: t("智能体管理"), run: () => switchTab("agents") },
10753
+ { icon: "i-cpu", label: t("本机智能体"), run: () => switchTab("agents") },
10730
10754
  { icon: "i-chart", label: t("用量统计"), run: () => switchTab("usage") },
10731
10755
  { icon: "i-gear", label: t("设置"), run: () => enterSettings() },
10732
10756
  { icon: "i-bee", label: t("帮助中心"), run: () => welcomeOpen() },
package/app/ui/i18n.js CHANGED
@@ -428,8 +428,14 @@
428
428
  "%)": "%)",
429
429
  "(暂无数据)": "(No data yet)",
430
430
  "智能体管理": "Agents",
431
+ "本机智能体": "Local agents",
431
432
  "模型接入": "Models",
432
433
  "CLI 绑定": "CLI bindings",
434
+ "模型调度(可选)": "Model routing (optional)",
435
+ "自动推荐(推荐)": "Automatic recommendation (recommended)",
436
+ "确认禁用模型 {0}?链降级将自动跳过它,其余模型不受影响;可在模型调度页重新启用。": "Disable model {0}? Fallback routing will skip it; other models are unaffected. You can re-enable it under Model routing.",
437
+ "确认禁用该厂商?禁用后链降级自动跳过它,恢复后可在模型调度页重新启用。": "Disable this provider? Fallback routing will skip it. You can re-enable it under Model routing.",
438
+ " 个供应商?\n相关显式模型调度会自动解除,此操作不可撤销。": " providers?\nRelated explicit model routing overrides will be removed. This cannot be undone.",
433
439
  "编排设置": "Orchestrator",
434
440
  "经验库": "Skill library",
435
441
  "知识库": "Knowledge base",
@@ -503,9 +509,13 @@
503
509
  "一群 AI 智能体自动分工:规划、实现、评审、打磨,直到交付成果。": "A crew of AI agents splits up the work — plan, build, review, polish — until the result is delivered.",
504
510
  "添加模型供应商:设置 → 模型接入,填入 API Key": "Add a model provider: Settings → Models, paste your API key",
505
511
  "绑定 CLI 智能体:点「一键推荐绑定」自动配好": "Bind CLI agents: one click on Recommended binding configures them",
512
+ "启用本机智能体:运行时默认自动推荐模型": "Enable local agents: runtime model routing is automatic by default",
506
513
  "新建任务:选工作目录、写目标,蜂群开工": "Create a task: pick a work folder, describe the goal, and the hive gets going",
507
514
  "开始配置模型": "Set up models",
508
515
  "绑定 CLI 智能体": "Bind CLI agents",
516
+ "第三步:启用本机智能体": "Step 3: Enable local agents",
517
+ "无需预先绑定模型。CodeBee 默认按任务类型、难度、成本和健康状态自动推荐;需要固定厂商、模型或降级顺序时,再到「设置 → 模型调度(可选)」指定。": "You do not need to bind a model first. CodeBee recommends one from task type, difficulty, cost and health; use Settings → Model routing only when you need to pin a provider, model or fallback order.",
518
+ "还没有已安装且可编排的 CLI——先到「本机智能体」页安装并启用。": "No installed orchestration-ready CLI yet — install and enable one under Local agents.",
509
519
  "先跳过,直接体验": "Skip and explore",
510
520
  "可随时在「设置 → 关于与更新」重新打开本引导。": "You can reopen this guide anytime in Settings → About & updates.",
511
521
  "可随时按 F1,或在「设置 → 关于与更新」重新打开本帮助中心。": "Press F1 anytime, or reopen this help center in Settings → About & updates.",
@@ -761,6 +771,7 @@
761
771
  "手动:自行指定实现者与评审组": "Manual: pick implementer and reviewers yourself",
762
772
  "代码引擎": "Code engine",
763
773
  "评审引擎": "Review engine",
774
+ "直连引擎": "Direct engine",
764
775
  "实现 → 验证 → 评审": "Implement → verify → review",
765
776
  "起草 → 多维评审 → 门禁": "Draft → multi-dim review → gate",
766
777
  "大纲 → 逐章起草评审 → 合并(可续跑)": "Outline → per-chapter draft & review → merge (resumable)",
@@ -1005,6 +1016,7 @@
1005
1016
  "最多选 3 条(1 个主模型 + 2 个降级备选)。": "Up to 3 entries (1 primary + 2 fallbacks).",
1006
1017
  "有未保存改动": "Unsaved changes",
1007
1018
  "一键推荐绑定": "Auto-fill recommendations",
1019
+ "生成推荐指定方案": "Generate an override plan",
1008
1020
  "全部保存": "Save all",
1009
1021
  "已为 %1 个 CLI 预填推荐模型,确认无误后点各卡片「保存」,或点右上「全部保存」。": "Prefilled recommended models for %1 CLI(s). Review each card and click \"Save\", or \"Save all\" above.",
1010
1022
  "已为 %1 个空链 CLI 预填推荐,并修复 %2 条失效链——确认后保存。": "Prefilled %1 empty chain(s) and repaired %2 broken chain(s) — review and save.",
@@ -1016,6 +1028,11 @@
1016
1028
  "没有可推荐的:先到「模型接入」页导入与 CLI 协议匹配的供应商。": "Nothing to recommend — import providers matching the CLI protocols on the \"Models\" page first.",
1017
1029
  "所有 CLI 都已配置模型链,无需推荐。": "All CLIs already have model chains — no recommendation needed.",
1018
1030
  "一键绑定推荐模型": "Auto-bind recommended models",
1031
+ "写入推荐默认模型(可选)": "Write recommended CLI default (optional)",
1032
+ "默认不需要绑定:系统会按任务类型、难度、成本与健康状态自动推荐可用模型;只有需要固定厂商、模型或降级顺序时才在这里绑定。绑定后编排调用会注入该供应商的 API key 与地址;完全未指定时保持自动调度,没有兼容可用供应商才使用 CLI 自身配置。": "Binding is optional by default: the system recommends models from task type, difficulty, cost and health. Bind only to pin a provider, model or fallback order. With no override, routing stays automatic and uses the CLI's own configuration only when no compatible provider is available.",
1033
+ "(自动推荐厂商与模型;没有兼容可用供应商时才使用 CLI 自身配置)": " (automatically recommends a provider and model; uses the CLI's own configuration only when no compatible provider is available)",
1034
+ "这里管理本机 CLI 的安装、版本、启停、冒烟测试和 CLI 自己的默认模型。CodeBee 运行任务时默认自动推荐厂商与模型,无需绑定;只有需要固定厂商、模型或降级顺序时,才到「模型调度(可选)」页指定。安装命令不明的条目可直接编辑": "Manage local CLI installation, versions, enablement, smoke tests and each CLI's own default model here. CodeBee recommends providers and models automatically at runtime; use Model routing only when you need to pin a provider, model or fallback order. For entries without a known install command, edit",
1035
+ "默认无需绑定:CodeBee 会按任务类型、步骤角色、难度、成本、协议兼容性和健康状态动态推荐厂商与模型,并在无可用供应商时回落 CLI 自身配置。只有需要固定厂商、模型或跨厂商降级顺序时才在这里指定;显式指定会覆盖自动推荐,但不改写 CLI 全局配置。": "No binding is required by default. CodeBee dynamically recommends providers and models from task type, step role, difficulty, cost, protocol compatibility and health, then falls back to the CLI's own configuration when needed. Specify an override here only to pin a provider, model or cross-provider fallback order; overrides do not rewrite the CLI's global configuration.",
1019
1036
  "没有支持写入默认模型的已安装智能体。": "No installed agents support writing a default model.",
1020
1037
  "所有已安装智能体都已配置默认模型,无需绑定。": "All installed agents already have a default model — nothing to bind.",
1021
1038
  "已为 %1 个智能体写入推荐模型,%2 个失败。": "Wrote recommended models to %1 agent(s), %2 failed.",
@@ -1097,8 +1114,10 @@
1097
1114
  "先选一个供应商再测试连通": "Pick a provider first",
1098
1115
  "当前配置不生效:请检查供应商密钥、启停状态与模型选择。": "Current config is not active: check the API key, enabled state, and model selection.",
1099
1116
  "最大并发任务数(多任务同时跑、互不打扰)": "Max concurrent tasks (run in parallel, fully isolated)",
1117
+ "并发保护上限(默认立即运行)": "Concurrency safety limit (starts immediately by default)",
1100
1118
  "1 · 串行": "1 · serial",
1101
- "1 = 串行排队;任务多可开 6-12 并行。同一任务仍单飞(防双烧评审),跨任务互不打扰;排队超 2 分钟会自动补队自愈。": "1 = serial queue; open 6-12 when running many tasks. Same task stays single-flight (no double review); tasks don't block each other; anything stuck in queue over 2 minutes self-heals automatically.",
1119
+ "任务默认立即运行,不进入等待队列;达到上限时会明确提示稍后重试。同一任务仍单飞,自动续跑的退避等待不占执行位。": "Tasks start immediately by default with no waiting queue. At the safety limit, CodeBee clearly asks you to retry later. Each task remains single-flight, and auto-resume backoff uses no execution slot.",
1120
+ "正在启动": "Starting",
1102
1121
  "默认保存路径(新建任务未指定工作目录时使用;支持 ~)": "Default save path (used when a new task doesn't specify one; ~ supported)",
1103
1122
  "保存时把「旧默认路径下」的现有任务目录迁移到新路径(运行中的跳过;手动指定的目录不受影响)": "When saving, migrate existing task directories under the old default path to the new one (skip running tasks; manually specified paths are untouched)",
1104
1123
  "不沿用(全新开始)": "Don't resume (fresh start)",
@@ -2094,7 +2113,7 @@
2094
2113
  "「冷却中」是什么意思?": "What does \"Cooling down\" mean?",
2095
2114
  "一把 Key 连续失败会被暂停 30 分钟,防止反复撞墙烧钱,到期自动恢复;也可以在密钥旁手动重置。": "A key that keeps failing is paused for 30 minutes so it stops burning money against a wall; it recovers automatically when time is up, or reset it manually next to the key.",
2096
2115
  "任务一直显示「排队」?": "Task stuck on \"Queued\"?",
2097
- "本地默认最多 6 个任务同时跑,排满就排队。排队时可以打开详情看前面还有几个;个别卡死的任务,看门狗会自动清理补队。": "Up to 6 tasks run concurrently by default; extras queue up. Open a queued task's detail to see how many are ahead; a watchdog automatically cleans up stuck tasks and re-queues them.",
2116
+ "新任务默认立即运行,不会排队;达到并发保护上限时本次会直接失败并提示稍后重试。只有自动续跑退避会显示预定时间;历史版本遗留的排队记录会由恢复机制立即接管或收口。": "New tasks start immediately and never wait in a queue. At the concurrency safety limit, the attempt fails clearly and asks you to retry later. Only auto-resume backoff shows a scheduled time; legacy queued records are immediately resumed or closed by recovery.",
2098
2117
  "状态里写着「将于 HH:MM 自动续跑」?": "Status says \"Auto-resumes at HH:MM\"?",
2099
2118
  "这一步失败了,正在退避等待自动重试,到点会接着跑,不需要手动干预;等不及也可以在详情页手动重试。": "That step failed and is backing off before an automatic retry — it continues on its own at the shown time, no manual action needed; if you can't wait, retry manually from the detail page.",
2100
2119
  "生成的文件在哪?": "Where are the generated files?",
@@ -2216,7 +2235,7 @@
2216
2235
  "工作目录文件": "Workspace files",
2217
2236
  "备份不含运行过程日志": "Backup has no run process logs",
2218
2237
  "路径重映射": "Path remap",
2219
- "有任务正在运行或排队,先等它们结束再导入": "Tasks are running or queued — wait for them to finish before importing",
2238
+ "有任务正在运行或等待自动续跑,先等它们结束再导入": "Tasks are running or waiting for auto-resume — wait for them to finish before importing",
2220
2239
  "替换导入会先清空本机数据目录,再整体落入备份内容(当前数据已自动留了反悔备份)。确定继续?": "Replace-import wipes the local data directory first, then restores the backup wholesale (a regret backup of current data is saved automatically). Continue?",
2221
2240
  "确认导入该备份包?同名任务与配置将被覆盖。": "Import this backup? Tasks and settings with the same names will be overwritten.",
2222
2241
  "正在导入…": "Importing…",