codebee 0.1.20 → 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.
@@ -1077,7 +1077,7 @@ def _goal_text(bug, side=None):
1077
1077
 
1078
1078
 
1079
1079
  def _launch_fix(bug, profile, side, cfg):
1080
- """为一个 bug 的某一端建修复任务并入队。与 automation._launch_run 同一条链。"""
1080
+ """为一个 bug 的某一端建修复任务并立即启动。"""
1081
1081
  bid = bug.get("id")
1082
1082
  repo = _repo_of(profile, side)
1083
1083
  wd = str(repo.get("workdir") or "").strip() or settings.default_workdir()
@@ -1088,11 +1088,31 @@ def _launch_fix(bug, profile, side, cfg):
1088
1088
  payload["git_rev"] = str(repo["git_rev"]).strip()
1089
1089
  if str(repo.get("verify_command") or "").strip():
1090
1090
  payload["verify_command"] = str(repo["verify_command"]).strip()
1091
- task = store.create_task(payload)
1092
- run = store.create_run("orchestration", task["title"], task_id=task["id"])
1093
- store.update_task_status(task["id"], "queued")
1094
- jobs.enqueue({"kind": "orchestration", "run_id": run["id"], "task_id": task["id"]})
1095
- return task, run
1091
+ task = None
1092
+ run = None
1093
+ try:
1094
+ task = store.create_task(payload)
1095
+ run = store.create_run("orchestration", task["title"], task_id=task["id"])
1096
+ store.update_task_status(task["id"], "queued")
1097
+ jobs.enqueue({"kind": "orchestration", "run_id": run["id"],
1098
+ "task_id": task["id"]})
1099
+ return task, run
1100
+ except Exception:
1101
+ log.exception("zentao: 修复任务启动失败 bug=%s side=%s task=%s run=%s",
1102
+ bid, side, (task or {}).get("id"), (run or {}).get("id"))
1103
+ if run:
1104
+ try:
1105
+ store.update_run(run["id"], status="failed",
1106
+ error="禅道修复任务启动失败,本次未排队,请稍后重试",
1107
+ ended_at=time.strftime("%Y-%m-%d %H:%M:%S"))
1108
+ except Exception:
1109
+ log.exception("zentao: 修复运行失败收口失败 run=%s", run.get("id"))
1110
+ if task:
1111
+ try:
1112
+ store.update_task_status(task["id"], "failed")
1113
+ except Exception:
1114
+ log.exception("zentao: 修复任务失败收口失败 task=%s", task.get("id"))
1115
+ raise
1096
1116
 
1097
1117
 
1098
1118
  # ---------------------------------------------------------------- 回写文本
package/app/main.py CHANGED
@@ -972,7 +972,7 @@ class Handler(BaseHTTPRequestHandler):
972
972
  m = re.match(r"^/api/catalog/([^/]+)/launch$", path)
973
973
  if m:
974
974
  # 一键打开(web 类起服务+开浏览器 / console 类新终端窗口)。
975
- # 即时返回不走任务队列;body 可传 {"open": false} 供测试免开浏览器
975
+ # 即时返回不走任务执行器;body 可传 {"open": false} 供测试免开浏览器
976
976
  entry = catalog.by_id(m.group(1))
977
977
  if not entry:
978
978
  return self._json(404, {"error": "catalog 中无此条目"})
@@ -1713,9 +1713,13 @@ class Handler(BaseHTTPRequestHandler):
1713
1713
  ok, err, new_run = store.retry_task(run["task_id"])
1714
1714
  if ok:
1715
1715
  store.update_run(new_run["id"], op="qa", qa_text=text)
1716
- self._enqueue_run(new_run["id"], run["task_id"],
1717
- {"kind": "orchestration", "run_id": new_run["id"],
1718
- "task_id": run["task_id"]})
1716
+ started, start_err = self._enqueue_run(
1717
+ new_run["id"], run["task_id"],
1718
+ {"kind": "orchestration", "run_id": new_run["id"],
1719
+ "task_id": run["task_id"]})
1720
+ if not started:
1721
+ return self._json(503, {"error": start_err,
1722
+ "run_id": new_run["id"]})
1719
1723
  return self._json(200, {"ok": True, "message": msg,
1720
1724
  "qa_run": new_run["id"]})
1721
1725
  return self._json(200, {"ok": True, "message": msg})
@@ -2100,15 +2104,17 @@ class Handler(BaseHTTPRequestHandler):
2100
2104
  return self._json(status, resp)
2101
2105
 
2102
2106
  def _enqueue_run(self, run_id, task_id, job):
2103
- """入队失败时把已持久化记录收口到 failed,避免 UI 永远显示排队中。"""
2107
+ """立即启动失败时收口到 failed;系统默认没有等待队列。"""
2104
2108
  try:
2105
2109
  jobs.enqueue(job)
2106
2110
  return True, ""
2107
- except Exception:
2111
+ except Exception as exc:
2108
2112
  # 不把异常文本(本机路径、命令行参数、供应商响应)返回给客户端;
2109
2113
  # 详细堆栈只进服务端日志,run 记录也保留稳定的用户可读文案。
2110
2114
  log.exception("任务入队失败 run=%s task=%s", run_id, task_id)
2111
- err = "任务入队失败,请稍后重试"
2115
+ busy = isinstance(exc, jobs.JobsBusyError)
2116
+ err = ("当前运行任务已达并发保护上限,本次未排队,请稍后重试"
2117
+ if busy else "任务启动失败,请稍后重试")
2112
2118
  try:
2113
2119
  closed = store.update_run(run_id, status="failed", error=err,
2114
2120
  ended_at=time.strftime("%Y-%m-%d %H:%M:%S"))
@@ -2161,8 +2167,7 @@ def _state_payload(client_id="", ver=None):
2161
2167
  "control": remote.control_view(client_id),
2162
2168
  # 供应商健康/告警(顶栏横幅数据源;有告警时 bump_state 会推给所有端)
2163
2169
  "health": health.snapshot(),
2164
- # 任务队列观测(worker 池目标/存活 + 队列深度):排队问题排障一眼定位
2165
- # 是「并发满载在等」还是「job 蒸发没人管」(后者由看门狗 2 分钟自愈)
2170
+ # 直接执行观测:并发保护上限、运行数和可用位;queued 恒为 0。
2166
2171
  "jobs": jobs.workers_info(),
2167
2172
  }
2168
2173
 
@@ -2355,14 +2360,17 @@ def main():
2355
2360
  telemetry.start_background() # 匿名错误回传+版本 ping(默认开可关;未配端点自动休眠,延迟 45s 不挡启动)
2356
2361
  except Exception:
2357
2362
  pass
2358
- _step("正在启动任务队列…")
2363
+ _step("正在启动任务执行器…")
2359
2364
  jobs.start_worker()
2365
+ n_wait = jobs.restore_deferred_resumes()
2366
+ if n_wait:
2367
+ print("[CodeBee] 已恢复 %d 个定时退避中的自动续跑任务" % n_wait)
2360
2368
  n_resume = jobs.resume_interrupted() # 启动恢复:服务被杀中断的连载任务自动续跑
2361
2369
  if n_resume:
2362
2370
  print("[CodeBee] 已自动恢复 %d 个中断的连载任务(断点续跑)" % n_resume)
2363
- n_rq = jobs.requeue_pending() # 启动补队:队列在内存里,重启会让排队项变僵尸
2371
+ n_rq = jobs.requeue_pending() # 兼容旧版本遗留的无退避 queued 记录
2364
2372
  if n_rq:
2365
- print("[CodeBee] 已重新入队 %d 个遗留排队运行" % n_rq)
2373
+ print("[CodeBee] 已直接启动 %d 个遗留运行" % n_rq)
2366
2374
  _step("正在启动自动化调度…")
2367
2375
  n_auto = automation.start() # 自动化:加载定时任务并拉起调度线程(错过的一次性任务不补跑)
2368
2376
  if n_auto:
package/app/pet.py CHANGED
@@ -117,7 +117,7 @@ def derive_state(prev_active_ids, tasks):
117
117
 
118
118
 
119
119
  def tooltip_lines(snap, lang="zh"):
120
- """悬停清单:未读摘要 + 进行中 + 排队,最多 6 行。
120
+ """悬停清单:未读摘要 + 进行中 + 待启动/自动续跑,最多 6 行。
121
121
 
122
122
  只列「还在跑 / 待跑」的任务——历史失败项会刷满清单(截图实测十几条
123
123
  ✘ 把面板撑成一堵墙),失败已由警示气泡点名,这里不再重复。
@@ -167,7 +167,7 @@ LANG = {
167
167
  "all_clear": "蜂群闲着,都在打盹…",
168
168
  "title": "蜂群动态",
169
169
  "n_running": "%d 个进行中",
170
- "queued": "排队中",
170
+ "queued": "正在启动",
171
171
  "cheer": "🎉 %d 个任务完工!",
172
172
  "alert": "⚠️ 「%s」出岔子了,点我看看",
173
173
  "bye": "蜜蜂回巢啦",
package/app/ui/app.js CHANGED
@@ -426,13 +426,13 @@ function jsq(s) {
426
426
  }
427
427
 
428
428
  function statusChip(st) {
429
- 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("超时") };
430
430
  return '<span class="chip ' + esc(st) + '">' + (zh[st] || esc(st)) + "</span>";
431
431
  }
432
432
 
433
433
  /* 运行状态文案(传 run 对象):退避窗口内的续跑副本写明「将于 HH:MM 自动
434
434
  * 续跑」,别让 5 分钟等待看起来像卡死/资源排队(2026-09-18 重写任务误判案)。
435
- * 到点后翻回「排队中」——页面轮询重渲染时 Date.now() 已过预定时刻。 */
435
+ * 到点后翻回「正在启动」——页面轮询重渲染时 Date.now() 已过预定时刻。 */
436
436
  function runStatusText(run) {
437
437
  const st = String((run && run.status) || "");
438
438
  if (st === "queued" && run && run.resume_enqueue_at) {
@@ -440,7 +440,7 @@ function runStatusText(run) {
440
440
  if (!isNaN(at) && Date.now() < at)
441
441
  return t("将于 {0} 自动续跑", String(run.resume_enqueue_at).slice(11, 16));
442
442
  }
443
- return { queued: t("排队中"), running: t("运行中"), done: t("完成"),
443
+ return { queued: t("正在启动"), running: t("运行中"), done: t("完成"),
444
444
  failed: t("失败"), cancelled: t("已取消"), timeout: t("超时") }[st] || st;
445
445
  }
446
446
 
@@ -632,7 +632,7 @@ async function healthOp(op) {
632
632
  async function healthDisableModel(pid, model) {
633
633
  if (!pid || !model) { closeModal(); return; }
634
634
  const yes = await uiConfirm(
635
- t("确认禁用模型 {0}?链降级将自动跳过它,其余模型不受影响;可在 CLI 绑定页重新启用。").replace("{0}", pid + " · " + model),
635
+ t("确认禁用模型 {0}?链降级将自动跳过它,其余模型不受影响;可在模型调度页重新启用。").replace("{0}", pid + " · " + model),
636
636
  { title: t("禁用模型"), danger: true, ok: t("禁用") });
637
637
  if (!yes) return;
638
638
  try {
@@ -657,7 +657,7 @@ async function healthDisableModel(pid, model) {
657
657
  async function healthDisableProvider(pid) {
658
658
  if (!pid) { closeModal(); return; }
659
659
  const yes = await uiConfirm(
660
- t("确认禁用该厂商?禁用后链降级自动跳过它,恢复后可在 CLI 绑定页重新启用。"),
660
+ t("确认禁用该厂商?禁用后链降级自动跳过它,恢复后可在模型调度页重新启用。"),
661
661
  { title: t("禁用厂商"), danger: true, ok: t("禁用") });
662
662
  if (!yes) return;
663
663
  try {
@@ -855,7 +855,7 @@ function renderBindings() {
855
855
  }
856
856
  bbox.innerHTML = targets.map((c) => {
857
857
  const b = (S.bindings || {})[c.id] || {};
858
- const opts = '<option value="">' + t("不绑定(用 CLI 自身的凭据与配置)") + '</option>' + bindable.map((p) => {
858
+ const opts = '<option value="">' + t("自动推荐(推荐)") + '</option>' + bindable.map((p) => {
859
859
  const adaptedOnly = p.protocol !== "anthropic" && p.protocol !== "openai";
860
860
  return '<option value="' + esc(p.id) + '"' + (b.provider_id === p.id ? " selected" : "") + ">" +
861
861
  esc(p.name) + t("(") + esc(protoLabel(p)) + (adaptedOnly ? t(" · 已适配") : "") +
@@ -879,13 +879,13 @@ function renderBindings() {
879
879
  return '<div class="card"><div class="head"><span class="name">' + esc(c.name) + "</span>" +
880
880
  '<span class="tag">' + esc(c.orch_kind) + "</span></div>" +
881
881
  '<div class="field"><label>' + t("供应商") + '</label><select id="bindprov-' + esc(c.id) + '">' + opts + "</select></div>" +
882
- '<p class="hint">' + t("绑定后编排调用会注入该供应商的 API key 与地址;不绑定则只按下方模型链传 -m 参数。") + '</p>' +
882
+ '<p class="hint">' + t("默认不需要绑定:系统会按任务类型、难度、成本与健康状态自动推荐可用模型;只有需要固定厂商、模型或降级顺序时才在这里绑定。绑定后编排调用会注入该供应商的 API key 与地址;完全未指定时保持自动调度,没有兼容可用供应商才使用 CLI 自身配置。") + '</p>' +
883
883
  bindModelBox(c, b.provider_id) + offWarn + protoWarn + chainWarn +
884
884
  '<div class="ops"><label class="toggle"><input type="checkbox" id="binddiff-' + esc(c.id) + '"' +
885
885
  (b.difficulty_routing ? " checked" : "") + '>' + t(" 按难度自动选模型(简单/困难)") + '</label>' +
886
886
  '<button class="ghost small" onclick="saveBinding(\'' + esc(c.id) + '\')">' + t("保存") + '</button></div></div>';
887
887
  }).join("") +
888
- (!targets.length ? '<p class="hint">' + t("还没有已安装且可编排的 CLI——先到「智能体管理」页安装并启用。") + '</p>' : "") +
888
+ (!targets.length ? '<p class="hint">' + t("还没有已安装且可编排的 CLI——先到「本机智能体」页安装并启用。") + '</p>' : "") +
889
889
  (nonBindable ? '<p class="hint">' + t("另有 ") + nonBindable +
890
890
  t(" 个供应商(google 等协议)仅登记,不支持注入 CLI,未出现在上面的下拉中。") + '</p>' : "");
891
891
  }
@@ -974,7 +974,7 @@ async function batchProvOp(op) {
974
974
  const tips = {
975
975
  enable: t("启用所选 ") + ids.length + t(" 个供应商?"),
976
976
  disable: t("停用所选 ") + ids.length + t(" 个供应商?\n停用后其绑定会回落为 CLI 默认;配置与模型列表都保留,可随时再启用。"),
977
- delete: t("删除所选 ") + ids.length + t(" 个供应商?\n相关 CLI 绑定会自动解绑,此操作不可撤销。"),
977
+ delete: t("删除所选 ") + ids.length + t(" 个供应商?\n相关显式模型调度会自动解除,此操作不可撤销。"),
978
978
  };
979
979
  if (!await uiConfirm(tips[op] || (t("执行「") + op + t("」?")))) return;
980
980
  try {
@@ -1780,11 +1780,15 @@ async function doImport() {
1780
1780
 
1781
1781
  async function saveBinding(id) {
1782
1782
  const st = bindSelById(id);
1783
+ const provSel = $("bindprov-" + id);
1783
1784
  // 主供应商跟链首走:模型链是唯一真源,链首非空时供应商下拉必须与之一致
1784
1785
  // (下拉是旧状态时把旧值发上去,后端「显式指定」就会盖回停用的旧供应商)。
1785
- const headP = st.chain.length ? st.chain[0].p : "";
1786
- const provSel = $("bindprov-" + id);
1787
- 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
+ }
1788
1792
  try {
1789
1793
  await api("/api/models/binding", { method: "POST", body: JSON.stringify({
1790
1794
  agent_id: id, provider_id: provSel ? provSel.value : "",
@@ -1926,7 +1930,8 @@ async function createTask() {
1926
1930
  msg.textContent = t("目标有点简短,先问几个问题…");
1927
1931
  try {
1928
1932
  const cq = await api("/api/tasks/clarify", {
1929
- 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 }) });
1930
1935
  const qs = (cq && cq.questions) || [];
1931
1936
  if (qs.length) {
1932
1937
  S.clarifyDone = true; // 本轮已采访;再点发送直接创建
@@ -1984,7 +1989,8 @@ async function createTask() {
1984
1989
  if (critics.length) payload.critics = critics;
1985
1990
  }
1986
1991
  try {
1987
- 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) });
1988
1994
  msg.textContent = t("已创建,跳转运行页…");
1989
1995
  // 先把新任务刷进 state 再跳:chatEngineIsDirect 靠 S.state.tasks 判引擎,
1990
1996
  // 不刷的话对话页签不会就绪,自动选卡落不到「对话」
@@ -3260,7 +3266,7 @@ function drawTaskDetail(key, runs) {
3260
3266
  // 取消收尾把僵尸步骤落成「已取消」时要立即重画,不等条数变化;
3261
3267
  // 作品信息状态入签名:后台一键生成 running→done 要立刻反映到成果区面板
3262
3268
  const bmTask = ((S.state || {}).tasks || []).find((x) => x.id === key);
3263
- // 自动续跑退避相位入签名:预定入队时刻过了之后 chip 要从「将于 HH:MM」翻回「排队中」
3269
+ // 自动续跑退避相位入签名:预定启动时刻过了之后 chip 翻为「正在启动」
3264
3270
  const lr0 = runs[0] || {};
3265
3271
  const resumePending = (lr0.resume_enqueue_at &&
3266
3272
  Date.now() < Date.parse(String(lr0.resume_enqueue_at).replace(" ", "T"))) ? 1 : 0;
@@ -3274,8 +3280,8 @@ function drawTaskDetail(key, runs) {
3274
3280
  const ordered = runs.slice(); // 详情按最近运行优先,方便排查
3275
3281
  const totalSteps = runs.reduce((a, r) => a + (r.steps || []).length, 0);
3276
3282
  const active = runs.some((r) => r.status === "running" || r.status === "queued");
3277
- // 活跃态细分真实状态:排队里还分「等并发」和「自动续跑退避(预定 HH:MM 入队)」,
3278
- // 后者在 chip 上写明下一轮何时起跑,别让 5 分钟退避窗口看起来像卡死(Z.ai 误伤案)
3283
+ // 活跃态细分真实状态:queued 仅用于自动续跑退避或创建到起跑的瞬时状态;
3284
+ // 前者在 chip 上写明下一轮何时起跑,避免退避窗口看起来像卡死。
3279
3285
  const activeRun0 = runs.find((r) => r.status === "running" || r.status === "queued");
3280
3286
  const st = activeRun0 ? activeRun0.status : latest.status;
3281
3287
  const resumeIn = (st === "queued" && resumePending)
@@ -3285,7 +3291,7 @@ function drawTaskDetail(key, runs) {
3285
3291
  chip.className = "chip " + st;
3286
3292
  chip.textContent = resumeIn
3287
3293
  ? t("将于 ") + resumeIn + t(" 自动续跑(第 ") + (Number(latest.auto_resumes) || 0) + t(" 次)")
3288
- : ({ 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);
3289
3295
  const bpt = $("btn-pause");
3290
3296
  if (bpt) bpt.classList.add("hidden");
3291
3297
  $("btn-delete").classList.add("hidden");
@@ -6057,12 +6063,17 @@ function chatResultHTML(run, res) {
6057
6063
  if (res.turns) meta.push(res.turns + " " + t("轮对话"));
6058
6064
  if (res.duration_s != null) meta.push(chatDurTxt(res.duration_s));
6059
6065
  const files = res.files || [];
6060
- const chips = files.map((f) =>
6061
- '<a class="file-chip chat-file-open" href="' + urlAuth("/api/runs/" + encodeURIComponent(run.id) + "/file?name=" +
6062
- encodeURIComponent(f.name)) + '" target="_blank" rel="noopener" title="' +
6063
- 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) + '">' +
6064
- '<i class="fx">' + esc(_fpExt(f.name).slice(0, 4) || "file") + "</i>" +
6065
- '<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("");
6066
6077
  return '<div class="chat-row">' +
6067
6078
  '<span class="chat-avatar" aria-hidden="true"><svg class="ico"><use href="' + icon + '"></use></svg></span>' +
6068
6079
  '<div class="chat-result' + (ok ? "" : bad ? " bad" : " off") + '">' +
@@ -6877,12 +6888,11 @@ function chainDeadReasons(c, chain) {
6877
6888
  return dead;
6878
6889
  }
6879
6890
 
6880
- /* 单条绑定链的推荐修复动作:需要改返回新链,不动返回 null。
6881
- * 空链 → 预填推荐;整条死透(或只有链首且已死)→ 重推荐;链首死但链内还有
6882
- * 活的备选 新链首插最前(原降级序保留,推荐项已在链内则升首去重)。 */
6883
- function bindRepairAction(c, st) {
6891
+ /* 单条显式绑定链的推荐修复动作:需要改返回新链,不动返回 null。
6892
+ * 空链代表默认自动调度,不能悄悄写成持久绑定;已有链失效时才推荐替代链。 */
6893
+ function bindRepairAction(c, st, includeEmpty) {
6884
6894
  const rec = recommendFor(c);
6885
- if (!st.chain.length) return rec ? [rec] : null;
6895
+ if (!st.chain.length) return includeEmpty && rec ? [rec] : null;
6886
6896
  const dead = chainDeadReasons(c, st.chain);
6887
6897
  const allDead = dead.every((d) => d);
6888
6898
  const headDead = dead[0] !== ""; // 空 p = CLI 默认凭据,是有意配置不算死
@@ -6897,7 +6907,7 @@ function autoBindAll() {
6897
6907
  let filled = 0, skipped = 0, noProv = 0, refilled = 0;
6898
6908
  for (const c of targets) {
6899
6909
  const st = bindSelById(c.id);
6900
- const act = bindRepairAction(c, st);
6910
+ const act = bindRepairAction(c, st, true);
6901
6911
  if (!act) {
6902
6912
  // 没动它:分清「健康链无需推荐」和「没有可推荐的」两种落空
6903
6913
  if (!st.chain.length) { noProv++; continue; }
@@ -6926,9 +6936,8 @@ function autoBindAll() {
6926
6936
  }
6927
6937
  }
6928
6938
 
6929
- /* 厂商/模型停用·启用·删除后自动补一次推荐绑定(2026-09-17 用户拍板):
6930
- * 绑定链——空链预填、死链重推荐、死链首插新首,与「一键推荐绑定」同规则,
6931
- * 但直接落盘(自动场景没有人工确认环节);目录页——空默认模型直填。
6939
+ /* 厂商/模型停用·启用·删除后自动修复显式绑定:
6940
+ * 空链保持自动调度,不写入绑定;已有链失效时才直接落盘替代链。
6932
6941
  * 没有合适的推荐就保持原样,什么都不绑。绑定页上用户手改中的草稿(dirty)
6933
6942
  * 不碰;一处都没改成静默返回,不打扰停用/启用的操作反馈。 */
6934
6943
  let _autoRebindRunning = false, _autoRebindAgain = false;
@@ -6941,7 +6950,7 @@ async function autoRebindSoon() {
6941
6950
  for (const c of (S.catalog || []).filter((x) => x.installed && x.orch_kind)) {
6942
6951
  const st = bindSelById(c.id);
6943
6952
  if (st.dirty) continue; // 用户手改中,不覆盖草稿
6944
- const act = bindRepairAction(c, st);
6953
+ const act = bindRepairAction(c, st, false);
6945
6954
  if (!act) continue;
6946
6955
  const b = (S.bindings || {})[c.id] || {};
6947
6956
  try {
@@ -6953,17 +6962,6 @@ async function autoRebindSoon() {
6953
6962
  fixed++;
6954
6963
  } catch (e) { /* 单条失败不打断,等下次变更再补 */ }
6955
6964
  }
6956
- for (const c of (S.catalog || []).filter((x) =>
6957
- x.installed && x.config_writable && !fmtModel(x.model))) {
6958
- const rec = recommendFor(c);
6959
- if (!rec) continue;
6960
- try {
6961
- const r = await api("/api/catalog/" + encodeURIComponent(c.id) + "/model",
6962
- { method: "POST", body: JSON.stringify({ model: rec.m }) });
6963
- c.model = r.model || rec.m;
6964
- fixed++;
6965
- } catch (e) { /* 同上 */ }
6966
- }
6967
6965
  if (fixed) {
6968
6966
  S.catSig = null; S.bindSig = null;
6969
6967
  render();
@@ -7024,7 +7022,7 @@ function bindModelBox(c, provId) {
7024
7022
  }).join("")
7025
7023
  : '<span class="hint">' + t("未设置") + (provId
7026
7024
  ? t("(按供应商/难度自动解析——供应商协议不匹配或被停用时解析为空,相关步骤将判失败)")
7027
- : t("(用 CLI 默认模型——不会注入任何供应商凭据)")) + "</span>";
7025
+ : t("(自动推荐厂商与模型;没有兼容可用供应商时才使用 CLI 自身配置)")) + "</span>";
7028
7026
  return '<div class="field"><label>' + t("运行时模型链(跨厂商,最多 ") + MAX_ORCH_MODELS + t(" 条)") + "</label>" +
7029
7027
  '<div class="orch-row">' + chips +
7030
7028
  '<button class="ghost small" onclick="bindToggle(\'' + esc(c.id) + '\')">' +
@@ -7827,11 +7825,11 @@ function autoTplPace(tp) {
7827
7825
  });
7828
7826
  }
7829
7827
 
7830
- /* last_status → 徽章:queued=正常灰、error=红「拉起失败」、missed=黄「已错过」 */
7828
+ /* last_status → 徽章:started=正常灰、error=红「拉起失败」、missed=黄「已错过」 */
7831
7829
  function autoStatusTag(tsk) {
7832
7830
  if (tsk.last_status === "error") return '<span class="tag auto-tag-err">' + t("拉起失败") + "</span>";
7833
7831
  if (tsk.last_status === "missed") return '<span class="tag auto-tag-miss">' + t("已错过") + "</span>";
7834
- 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>";
7835
7833
  return "";
7836
7834
  }
7837
7835
 
@@ -8490,7 +8488,11 @@ function mkrDebounce() {
8490
8488
 
8491
8489
  /* 拉一页:reset=清空已累积列表(首进/搜索/换来源/刷新后)。 */
8492
8490
  async function mkrLoadPage(reset) {
8493
- if (S.mkrLoading) return;
8491
+ // 翻页保持单飞;搜索/换来源/刷新属于重置请求,可以抢占旧请求。
8492
+ // 旧响应回来时由 requestKey 丢弃,不能覆盖用户刚选的新条件。
8493
+ if (S.mkrLoading && !reset) return;
8494
+ const requestKey = (S.mkrRequestKey || 0) + 1;
8495
+ S.mkrRequestKey = requestKey;
8494
8496
  if (reset) { S.mkrAll = []; S.mkrOffset = 0; }
8495
8497
  const q = (($("mkr-search") || {}).value || "").trim();
8496
8498
  const srcSel = $("mkr-source");
@@ -8500,6 +8502,7 @@ async function mkrLoadPage(reset) {
8500
8502
  S.mkrLoading = true;
8501
8503
  let data = null;
8502
8504
  try { data = await api(url); } catch (e) { data = null; }
8505
+ if (requestKey !== S.mkrRequestKey) return;
8503
8506
  S.mkrLoading = false;
8504
8507
  if (!data) { S.marketRemote = null; S.mkrAll = null; renderMarketRemote(); return; }
8505
8508
  S.marketRemote = data;
@@ -8587,7 +8590,8 @@ async function mkrRefresh() {
8587
8590
  const old = btn.textContent;
8588
8591
  btn.textContent = t("拉取中…");
8589
8592
  try {
8590
- 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: "{}" });
8591
8595
  const bad = (r.refresh || []).filter((x) => !x.ok);
8592
8596
  if (bad.length) toast(t("部分来源拉取失败:") + bad.map((x) => x.error || x.id).join(t(";")), true);
8593
8597
  else toast(t("拉取成功"));
@@ -8920,13 +8924,13 @@ function helpChapterBody(id) {
8920
8924
  if (id === "quickstart") {
8921
8925
  return '<div class="welcome-steps">' +
8922
8926
  '<div class="wstep"><b>1</b><span>' + t("添加模型供应商:设置 → 模型接入,填入 API Key") + '</span></div>' +
8923
- '<div class="wstep"><b>2</b><span>' + t("绑定 CLI 智能体:点「一键推荐绑定」自动配好") + '</span></div>' +
8927
+ '<div class="wstep"><b>2</b><span>' + t("启用本机智能体:运行时默认自动推荐模型") + '</span></div>' +
8924
8928
  '<div class="wstep"><b>3</b><span>' + t("新建任务:选工作目录、写目标,蜂群开工") + '</span></div>' +
8925
8929
  '</div>' +
8926
8930
  '<p class="help-note">' + t("任务跑起来后,详情页能看到步骤、蜂巢、成果文件与 Git 版本;「直接执行」类任务还能像聊天一样边跑边追加消息。") + '</p>' +
8927
8931
  '<div class="welcome-acts">' +
8928
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>' +
8929
- '<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>' +
8930
8934
  '<button class="wl-btn ghost" onclick="welcomeClose()"><span>' + t("先跳过,直接体验") + '</span></button>' +
8931
8935
  '</div>';
8932
8936
  }
@@ -8935,8 +8939,8 @@ function helpChapterBody(id) {
8935
8939
  '<p class="help-p">' + t("「设置 → 模型接入 → 添加供应商」:填名称、API Key、接口地址(一般用默认)。保存后点「获取模型列表」自动拉取该厂商的模型;网关不提供列表接口时直接手工填模型名即可。同一厂商可配多把 Key,按顺序轮换,某把欠费自动切下一把。") + '</p>' +
8936
8940
  '<h3 class="help-h3">' + t("第二步:认识协议徽章") + '</h3>' +
8937
8941
  '<p class="help-p">' + t("新供应商默认自动识别协议,保存后在后台实测支持哪些调用形态,结果以徽章标在卡片上。带「· chat」表示该模型只适合站内直连对话;codex 这类只讲 responses 协议的 CLI 用不了它,绑定页会自动剔除,不用自己排查。") + '</p>' +
8938
- '<h3 class="help-h3">' + t("第三步:绑定 CLI 智能体") + '</h3>' +
8939
- '<p class="help-p">' + t("「设置CLI 绑定」给每个智能体配一条模型链,运行时按顺序故障转移。「一键推荐绑定」只填空缺,不覆盖手工配置;删除或停用厂商后链会自动补绑。") + '</p>' +
8942
+ '<h3 class="help-h3">' + t("第三步:启用本机智能体") + '</h3>' +
8943
+ '<p class="help-p">' + t("无需预先绑定模型。CodeBee 默认按任务类型、难度、成本和健康状态自动推荐;需要固定厂商、模型或降级顺序时,再到「设置 模型调度(可选)」指定。") + '</p>' +
8940
8944
  '<h3 class="help-h3">' + t("报错速查") + '</h3>' +
8941
8945
  '<ul class="help-list">' +
8942
8946
  '<li><b>401</b><span>' + t("Key 无效或过期——检查或更换 Key。") + '</span></li>' +
@@ -9001,7 +9005,7 @@ function helpChapterBody(id) {
9001
9005
  return '<div class="help-qa"><b>' + t("点「获取模型列表」报 404?") + '</b><p>' + t("部分网关不提供模型列表接口,属正常现象;只要能正常对话就不用管,模型名手工填即可。") + '</p></div>' +
9002
9006
  '<div class="help-qa"><b>' + t("报 503 / 无可用渠道?") + '</b><p>' + t("该模型当前没有可用渠道,最常见是余额耗尽。查一下余额或换个模型;同一厂商配了多把 Key 会自动轮换。") + '</p></div>' +
9003
9007
  '<div class="help-qa"><b>' + t("「冷却中」是什么意思?") + '</b><p>' + t("一把 Key 连续失败会被暂停 30 分钟,防止反复撞墙烧钱,到期自动恢复;也可以在密钥旁手动重置。") + '</p></div>' +
9004
- '<div class="help-qa"><b>' + t("任务一直显示「排队」?") + '</b><p>' + t("本地默认最多 6 个任务同时跑,排满就排队。排队时可以打开详情看前面还有几个;个别卡死的任务,看门狗会自动清理补队。") + '</p></div>' +
9008
+ '<div class="help-qa"><b>' + t("任务一直显示「排队」?") + '</b><p>' + t("新任务默认立即运行,不会排队;达到并发保护上限时本次会直接失败并提示稍后重试。只有自动续跑退避会显示预定时间;历史版本遗留的排队记录会由恢复机制立即接管或收口。") + '</p></div>' +
9005
9009
  '<div class="help-qa"><b>' + t("状态里写着「将于 HH:MM 自动续跑」?") + '</b><p>' + t("这一步失败了,正在退避等待自动重试,到点会接着跑,不需要手动干预;等不及也可以在详情页手动重试。") + '</p></div>' +
9006
9010
  '<div class="help-qa"><b>' + t("生成的文件在哪?") + '</b><p>' + t("写作类任务的章节、封面、报告都落在任务的运行目录,详情页「成果」页签可浏览和预览。代码类任务则在仓库的任务分支上改代码,详情页「版本」里审阅后再合并。") + '</p></div>' +
9007
9011
  '<div class="help-qa"><b>' + t("忘了令牌 / 手机打不开页面?") + '</b><p>' + t("服务启动日志里有带令牌的完整访问地址;远程设备必须用带令牌的 URL 打开(或在令牌门里输入一次),否则会一直要求授权。") + '</p></div>' +
@@ -9304,7 +9308,7 @@ async function suStartupCheck() {
9304
9308
  }
9305
9309
 
9306
9310
  /* ---------------------------------------------------------- 页签 & 初始化 */
9307
- 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: "关于与更新" };
9308
9312
  const SET_TABS = new Set(Object.keys(TAB_TITLES)); // 设置导航里的子页(__phone 是弹框,不算)
9309
9313
 
9310
9314
  function tabTitle(name) {
@@ -10062,7 +10066,7 @@ function renderBackupPreview(pv) {
10062
10066
  rows.push('<span class="bad">' + esc(t("以下外部目录不在备份里,需自行拷贝:"))
10063
10067
  + esc(pv.external_workdirs.join("、")) + "</span>");
10064
10068
  if ((pv.busy_runs || []).length)
10065
- rows.push('<span class="bad">' + esc(t("有任务正在运行或排队,先等它们结束再导入")) + "</span>");
10069
+ rows.push('<span class="bad">' + esc(t("有任务正在运行或等待自动续跑,先等它们结束再导入")) + "</span>");
10066
10070
  el.innerHTML = rows.map((s) => "<div>" + s + "</div>").join("");
10067
10071
  if (!(pv.busy_runs || []).length) $("bi-apply").classList.remove("hidden");
10068
10072
  }
@@ -10746,7 +10750,7 @@ function cmdkOps() {
10746
10750
  { icon: "i-blocks", label: t("插件市场"), run: () => switchTab("market") },
10747
10751
  { icon: "i-book", label: t("经验库"), run: () => switchTab("skills") },
10748
10752
  { icon: "i-sigma", label: t("知识库"), run: () => switchTab("knowledge") },
10749
- { icon: "i-cpu", label: t("智能体管理"), run: () => switchTab("agents") },
10753
+ { icon: "i-cpu", label: t("本机智能体"), run: () => switchTab("agents") },
10750
10754
  { icon: "i-chart", label: t("用量统计"), run: () => switchTab("usage") },
10751
10755
  { icon: "i-gear", label: t("设置"), run: () => enterSettings() },
10752
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.",
@@ -1006,6 +1016,7 @@
1006
1016
  "最多选 3 条(1 个主模型 + 2 个降级备选)。": "Up to 3 entries (1 primary + 2 fallbacks).",
1007
1017
  "有未保存改动": "Unsaved changes",
1008
1018
  "一键推荐绑定": "Auto-fill recommendations",
1019
+ "生成推荐指定方案": "Generate an override plan",
1009
1020
  "全部保存": "Save all",
1010
1021
  "已为 %1 个 CLI 预填推荐模型,确认无误后点各卡片「保存」,或点右上「全部保存」。": "Prefilled recommended models for %1 CLI(s). Review each card and click \"Save\", or \"Save all\" above.",
1011
1022
  "已为 %1 个空链 CLI 预填推荐,并修复 %2 条失效链——确认后保存。": "Prefilled %1 empty chain(s) and repaired %2 broken chain(s) — review and save.",
@@ -1017,6 +1028,11 @@
1017
1028
  "没有可推荐的:先到「模型接入」页导入与 CLI 协议匹配的供应商。": "Nothing to recommend — import providers matching the CLI protocols on the \"Models\" page first.",
1018
1029
  "所有 CLI 都已配置模型链,无需推荐。": "All CLIs already have model chains — no recommendation needed.",
1019
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.",
1020
1036
  "没有支持写入默认模型的已安装智能体。": "No installed agents support writing a default model.",
1021
1037
  "所有已安装智能体都已配置默认模型,无需绑定。": "All installed agents already have a default model — nothing to bind.",
1022
1038
  "已为 %1 个智能体写入推荐模型,%2 个失败。": "Wrote recommended models to %1 agent(s), %2 failed.",
@@ -1098,8 +1114,10 @@
1098
1114
  "先选一个供应商再测试连通": "Pick a provider first",
1099
1115
  "当前配置不生效:请检查供应商密钥、启停状态与模型选择。": "Current config is not active: check the API key, enabled state, and model selection.",
1100
1116
  "最大并发任务数(多任务同时跑、互不打扰)": "Max concurrent tasks (run in parallel, fully isolated)",
1117
+ "并发保护上限(默认立即运行)": "Concurrency safety limit (starts immediately by default)",
1101
1118
  "1 · 串行": "1 · serial",
1102
- "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",
1103
1121
  "默认保存路径(新建任务未指定工作目录时使用;支持 ~)": "Default save path (used when a new task doesn't specify one; ~ supported)",
1104
1122
  "保存时把「旧默认路径下」的现有任务目录迁移到新路径(运行中的跳过;手动指定的目录不受影响)": "When saving, migrate existing task directories under the old default path to the new one (skip running tasks; manually specified paths are untouched)",
1105
1123
  "不沿用(全新开始)": "Don't resume (fresh start)",
@@ -2095,7 +2113,7 @@
2095
2113
  "「冷却中」是什么意思?": "What does \"Cooling down\" mean?",
2096
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.",
2097
2115
  "任务一直显示「排队」?": "Task stuck on \"Queued\"?",
2098
- "本地默认最多 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.",
2099
2117
  "状态里写着「将于 HH:MM 自动续跑」?": "Status says \"Auto-resumes at HH:MM\"?",
2100
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.",
2101
2119
  "生成的文件在哪?": "Where are the generated files?",
@@ -2217,7 +2235,7 @@
2217
2235
  "工作目录文件": "Workspace files",
2218
2236
  "备份不含运行过程日志": "Backup has no run process logs",
2219
2237
  "路径重映射": "Path remap",
2220
- "有任务正在运行或排队,先等它们结束再导入": "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",
2221
2239
  "替换导入会先清空本机数据目录,再整体落入备份内容(当前数据已自动留了反悔备份)。确定继续?": "Replace-import wipes the local data directory first, then restores the backup wholesale (a regret backup of current data is saved automatically). Continue?",
2222
2240
  "确认导入该备份包?同名任务与配置将被覆盖。": "Import this backup? Tasks and settings with the same names will be overwritten.",
2223
2241
  "正在导入…": "Importing…",