codebee 0.1.21 → 0.1.23

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
@@ -297,6 +297,93 @@ function urlAuth(u) {
297
297
  }
298
298
 
299
299
  /* ---------------------------------------------------------- 工具 */
300
+ let _requestBusyCount = 0;
301
+ let _lastActionButton = null;
302
+ let _lastActionAt = 0;
303
+ const _requestBusyButtons = new WeakMap();
304
+
305
+ /* 记录发起请求的按钮。写请求统一在 api() 里挂忙碌态,避免每个业务入口各写一套;
306
+ * 确认框的按钮不覆盖原始操作按钮,但会续期,使“确认后执行”仍反馈在原按钮上。 */
307
+ document.addEventListener("click", (e) => {
308
+ const btn = e.target && e.target.closest ? e.target.closest("button") : null;
309
+ if (!btn) return;
310
+ if (btn.classList.contains("request-busy")) {
311
+ e.preventDefault();
312
+ e.stopImmediatePropagation();
313
+ return;
314
+ }
315
+ _lastActionAt = performance.now();
316
+ if (!btn.closest("#ask")) _lastActionButton = btn;
317
+ }, true);
318
+
319
+ function requestBusyStart(opts) {
320
+ const method = String((opts && opts.method) || "GET").toUpperCase();
321
+ if (opts && opts.busy === false) return null;
322
+ if ((method === "GET" || method === "HEAD") && !(opts && opts.busy)) return null;
323
+
324
+ _requestBusyCount++;
325
+ let bar = $("request-progress");
326
+ if (!bar && document.body) {
327
+ bar = document.createElement("div");
328
+ bar.id = "request-progress";
329
+ bar.className = "request-progress";
330
+ bar.setAttribute("role", "status");
331
+ bar.setAttribute("aria-label", t("操作处理中"));
332
+ bar.setAttribute("aria-hidden", "true");
333
+ document.body.appendChild(bar);
334
+ }
335
+ if (bar) {
336
+ bar.classList.add("active");
337
+ bar.setAttribute("aria-hidden", "false");
338
+ }
339
+
340
+ let btn = opts && opts.busyElement;
341
+ if (typeof btn === "string") btn = document.querySelector(btn);
342
+ if (!btn && performance.now() - _lastActionAt < 2000) btn = _lastActionButton;
343
+ if (!btn || !document.documentElement.contains(btn)) btn = null;
344
+ if (btn) {
345
+ let state = _requestBusyButtons.get(btn);
346
+ if (!state) {
347
+ state = {
348
+ count: 0,
349
+ ariaBusy: btn.getAttribute("aria-busy"),
350
+ ariaDisabled: btn.getAttribute("aria-disabled"),
351
+ };
352
+ _requestBusyButtons.set(btn, state);
353
+ }
354
+ state.count++;
355
+ btn.classList.add("request-busy");
356
+ btn.setAttribute("aria-busy", "true");
357
+ btn.setAttribute("aria-disabled", "true");
358
+ }
359
+ return { button: btn };
360
+ }
361
+
362
+ function requestBusyEnd(token) {
363
+ if (!token) return;
364
+ _requestBusyCount = Math.max(0, _requestBusyCount - 1);
365
+ const btn = token.button;
366
+ const state = btn && _requestBusyButtons.get(btn);
367
+ if (state) {
368
+ state.count--;
369
+ if (state.count <= 0) {
370
+ btn.classList.remove("request-busy");
371
+ if (state.ariaBusy === null) btn.removeAttribute("aria-busy");
372
+ else btn.setAttribute("aria-busy", state.ariaBusy);
373
+ if (state.ariaDisabled === null) btn.removeAttribute("aria-disabled");
374
+ else btn.setAttribute("aria-disabled", state.ariaDisabled);
375
+ _requestBusyButtons.delete(btn);
376
+ }
377
+ }
378
+ if (_requestBusyCount === 0) {
379
+ const bar = $("request-progress");
380
+ if (bar) {
381
+ bar.classList.remove("active");
382
+ bar.setAttribute("aria-hidden", "true");
383
+ }
384
+ }
385
+ }
386
+
300
387
  async function api(path, opts) {
301
388
  opts = opts || {};
302
389
  // 可选超时:长请求(如外部插件下载)传 opts.timeout,网络卡死时也能
@@ -306,21 +393,30 @@ async function api(path, opts) {
306
393
  ctrl = new AbortController();
307
394
  var timer = setTimeout(() => ctrl.abort(), opts.timeout);
308
395
  }
309
- let res;
396
+ const busyToken = requestBusyStart(opts);
397
+ const fetchOpts = Object.assign({}, opts);
398
+ delete fetchOpts.timeout;
399
+ delete fetchOpts.busy;
400
+ delete fetchOpts.busyElement;
310
401
  try {
311
- res = await fetch(path, Object.assign({ headers: authHeaders() }, opts, ctrl ? { signal: ctrl.signal } : {}));
312
- } catch (e) {
313
- if (ctrl && e.name === "AbortError") throw new Error(t("请求超时,请重试或检查网络"));
314
- throw e;
402
+ let res;
403
+ try {
404
+ res = await fetch(path, Object.assign({ headers: authHeaders() }, fetchOpts,
405
+ ctrl ? { signal: ctrl.signal } : {}));
406
+ } catch (e) {
407
+ if (ctrl && e.name === "AbortError") throw new Error(t("请求超时,请重试或检查网络"));
408
+ throw e;
409
+ }
410
+ if (res.status === 401) { showTokenGate(t("令牌不正确或已更换,请重新输入")); throw new Error(t("需要访问令牌")); }
411
+ let data = null;
412
+ try { data = await res.json(); } catch (e) { /* ignore */ }
413
+ if (res.status === 423 && data && data.control) setControl(data.control);
414
+ if (!res.ok) throw new Error((data && data.error) || ("HTTP " + res.status));
415
+ return data;
315
416
  } finally {
316
417
  if (timer) clearTimeout(timer);
418
+ requestBusyEnd(busyToken);
317
419
  }
318
- if (res.status === 401) { showTokenGate(t("令牌不正确或已更换,请重新输入")); throw new Error(t("需要访问令牌")); }
319
- let data = null;
320
- try { data = await res.json(); } catch (e) { /* ignore */ }
321
- if (res.status === 423 && data && data.control) setControl(data.control);
322
- if (!res.ok) throw new Error((data && data.error) || ("HTTP " + res.status));
323
- return data;
324
420
  }
325
421
 
326
422
  /* 轻提示:3.5s 自动消失 */
@@ -783,7 +879,7 @@ async function ctrlClick() {
783
879
  function startCtrlHeartbeat() {
784
880
  setInterval(() => {
785
881
  if (S.control && S.control.mine) {
786
- api("/api/control/heartbeat", { method: "POST", body: "{}" })
882
+ api("/api/control/heartbeat", { method: "POST", body: "{}", busy: false })
787
883
  .then((d) => setControl(d.control)).catch(() => {});
788
884
  }
789
885
  }, 15000);
@@ -1687,7 +1783,7 @@ async function openImportDialog() {
1687
1783
  '<button class="ghost" onclick="closeModal()">' + t("取消") + '</button>');
1688
1784
  let data;
1689
1785
  try {
1690
- data = await api("/api/models/sources");
1786
+ data = await api("/api/models/sources", { busy: true });
1691
1787
  } catch (e) {
1692
1788
  $("modal-body").innerHTML = '<div class="msg bad">' + t("扫描失败:") + esc(e.message) + "</div>";
1693
1789
  return;
@@ -2586,7 +2682,7 @@ async function retryTask(id) {
2586
2682
  async function continueSerial(id) {
2587
2683
  let info;
2588
2684
  try {
2589
- info = await api("/api/tasks/" + encodeURIComponent(id) + "/continue-info");
2685
+ info = await api("/api/tasks/" + encodeURIComponent(id) + "/continue-info", { busy: true });
2590
2686
  } catch (e) { toast(t("无法续写:") + e.message, true); return; }
2591
2687
  if (!info || !info.can) {
2592
2688
  toast(t("无法续写:") + ((info && info.reason) || t("当前状态不支持")), true);
@@ -3324,6 +3420,7 @@ function drawTaskDetail(key, runs) {
3324
3420
  '<span class="stat">tokens <b>' + sum("tokens") + "</b></span>" +
3325
3421
  (latest.error ? '<span class="stat err">' + errTag(latest.error) + esc(latest.error.slice(0, 200)) + "</span>" : "");
3326
3422
  $("rd-plan").classList.add("hidden");
3423
+ renderRouting(latest);
3327
3424
  let html = "";
3328
3425
  ordered.forEach((r, i) => {
3329
3426
  const runNo = runs.length - i;
@@ -3704,6 +3801,7 @@ async function renderRunDetail() {
3704
3801
  '<span class="stat tasksum hidden" id="rd-meta-task"></span>';
3705
3802
  if (S.detailSide) fillMetaTask(S.detailSide.stats || {}); // 缓存命中:轮询重画不闪丢累计组
3706
3803
  renderPlan(run);
3804
+ renderRouting(run);
3707
3805
  // 详情页优先展示最近一步,排查运行中的任务时无需滚到底部;
3708
3806
  // 蜂巢泳道仍按原始流程顺序呈现,避免破坏阶段语义。
3709
3807
  $("rd-steps").innerHTML = (run.steps || []).slice().reverse().map((s) =>
@@ -5347,6 +5445,75 @@ function renderPlan(run) {
5347
5445
  '<span class="role">' + esc(s.title || "") + "</span>" +
5348
5446
  '<span class="sum">' + esc(s.detail || "") + "</span></div>").join("") + "</div>" + routeHtml;
5349
5447
  }
5448
+
5449
+ /* 运行详情的统一调度审计:任务画像和实际选路来自同一次编译结果。
5450
+ * 旧运行没有 task_spec/route_plan 时保持隐藏,避免把历史数据伪装成当前决策。 */
5451
+ function renderRouting(run) {
5452
+ let box = $("rd-routing");
5453
+ if (!box) {
5454
+ const steps = $("rd-steps");
5455
+ if (!steps || !steps.parentNode) return;
5456
+ box = document.createElement("div");
5457
+ box.id = "rd-routing";
5458
+ box.className = "rd-routing hidden";
5459
+ steps.parentNode.insertBefore(box, steps);
5460
+ }
5461
+ const spec = run && run.task_spec;
5462
+ const plan = run && run.route_plan;
5463
+ if (!spec || !plan) { box.classList.add("hidden"); box.innerHTML = ""; return; }
5464
+ const type = spec.type || "direct";
5465
+ const difficulty = spec.difficulty || "default";
5466
+ const dimension = spec.dimension || "reasoning";
5467
+ const caps = Array.isArray(spec.capabilities) ? spec.capabilities.filter(Boolean) : [];
5468
+ const typeLabel = ((flowById(type) || {}).name) || type;
5469
+ const difficultyLabel = t(({ easy: "简单", default: "标准", hard: "困难" })[difficulty] || difficulty);
5470
+ const dimensionLabel = t(({ coding: "编码", writing: "写作", reasoning: "推理", vision: "视觉" })[dimension] || dimension);
5471
+ const capLabels = {
5472
+ coding: "编码", writing: "写作", reasoning: "推理", vision: "视觉",
5473
+ filesystem: "文件读写", verification: "验证", long_context: "长上下文",
5474
+ continuity: "连续性", attachments: "附件",
5475
+ };
5476
+ const capsText = caps.length ? caps.map((x) => t(capLabels[x] || x)).join(t("、")) : t("通用能力");
5477
+ const selectedLabel = (entry) => entry && (entry.label || entry.agent_id || "") || t("未选定");
5478
+ const roleBlock = (key, title) => {
5479
+ const route = plan[key] || {};
5480
+ const candidates = Array.isArray(route.candidates) ? route.candidates : [];
5481
+ if (!route.selected && !candidates.length) return "";
5482
+ const selected = candidates.find((x) => x.agent_id === route.selected) || candidates[0];
5483
+ const participants = Array.isArray(route.participants) ? route.participants : [];
5484
+ const activeIds = participants.length ? participants.slice() : (route.selected ? [route.selected] : []);
5485
+ if (route.selected && activeIds.includes(route.selected)) {
5486
+ activeIds.splice(activeIds.indexOf(route.selected), 1);
5487
+ activeIds.unshift(route.selected);
5488
+ }
5489
+ const activeLabels = activeIds.map((id) => {
5490
+ const item = candidates.find((x) => x.agent_id === id);
5491
+ return selectedLabel(item || { agent_id: id });
5492
+ }).filter(Boolean);
5493
+ const fallback = Array.isArray(route.fallback) ? route.fallback : [];
5494
+ const fallbackLabels = fallback.map((id) => {
5495
+ const item = candidates.find((x) => x.agent_id === id);
5496
+ return selectedLabel(item || { agent_id: id });
5497
+ }).filter(Boolean);
5498
+ const rows = candidates.map((x) =>
5499
+ '<div class="rd-route-row"><span class="rd-route-score">' + esc(Number(x.score || 0).toFixed(1)) +
5500
+ '</span><span class="rd-route-agent">' + esc(selectedLabel(x)) + '</span><span class="rd-route-reason">' +
5501
+ esc(x.reason || "") + '</span></div>').join("");
5502
+ return '<div class="rd-route-role"><div class="rd-route-role-head"><b>' + esc(title) +
5503
+ '</b><span>' + esc(activeLabels.join(" + ") || selectedLabel(selected)) + '</span></div>' +
5504
+ (route.selection_reason ? '<div class="rd-route-selection">' + esc(route.selection_reason) + '</div>' : "") +
5505
+ (fallbackLabels.length ? '<div class="rd-route-fallback">' + esc(t("降级:")) + esc(fallbackLabels.join(" → ")) + '</div>' : "") +
5506
+ (rows ? '<details class="rd-route-candidates"><summary>' + esc(t("查看候选评分")) + '</summary>' + rows + '</details>' : "") +
5507
+ '</div>';
5508
+ };
5509
+ const roles = [roleBlock("implement", t("执行")), roleBlock("review", t("评审"))].filter(Boolean).join("");
5510
+ if (!roles) { box.classList.add("hidden"); box.innerHTML = ""; return; }
5511
+ box.classList.remove("hidden");
5512
+ box.innerHTML = '<div class="rd-routing-head"><span class="rd-routing-title">' + esc(t("任务画像与调度")) +
5513
+ '</span><span class="rd-routing-spec">' + esc(t("{0} · {1} · {2}", t(typeLabel), difficultyLabel, dimensionLabel)) + '</span></div>' +
5514
+ '<div class="rd-routing-caps"><span>' + esc(t("需要:")) + '</span>' + esc(capsText) + '</div>' +
5515
+ '<div class="rd-route-roles">' + roles + '</div>';
5516
+ }
5350
5517
  /* 日志框顶部状态条:运行中 → ● 实时(呼吸点 + 最后刷新时间);结束 → 灰字已结束。
5351
5518
  * 没有它,用户分不清"日志在刷但 CLI 暂无输出"和"刷新坏了"——两者长得一模一样。 */
5352
5519
  function logLiveBadge(live) {
@@ -6378,6 +6545,7 @@ function bindVoiceInput() {
6378
6545
  if (e.results[i].isFinal) text += e.results[i][0].transcript;
6379
6546
  }
6380
6547
  if (text) {
6548
+ text = voiceFixup(text);
6381
6549
  ta.value = (ta.value ? ta.value.replace(/\s+$/, "") + " " : "") + text;
6382
6550
  ta.dispatchEvent(new Event("input", { bubbles: true }));
6383
6551
  }
@@ -6389,6 +6557,24 @@ function bindVoiceInput() {
6389
6557
  });
6390
6558
  }
6391
6559
 
6560
+ /* 语音识别结果纠偏(借鉴 lexicon 个人词典):设置里维护的专有名词表
6561
+ * (localStorage orch.voice_terms,一行一个「错误→正确」或直接「术语」),
6562
+ * 识别结果做逐词替换。人名/书名/项目名是识别高频错位,词表是最小可用解。 */
6563
+ function voiceFixup(text) {
6564
+ let terms = [];
6565
+ try {
6566
+ const raw = localStorage.getItem("orch.voice_terms") || "";
6567
+ terms = raw.split("\n").map((l) => l.trim()).filter(Boolean);
6568
+ } catch (e) {}
6569
+ for (const t of terms) {
6570
+ const m = t.split("→");
6571
+ if (m.length === 2 && m[0].trim() && m[1].trim()) {
6572
+ text = text.split(m[0].trim()).join(m[1].trim());
6573
+ }
6574
+ }
6575
+ return text;
6576
+ }
6577
+
6392
6578
  /* ---------------------------------------------------------- 外观:皮肤 + 明暗(换肤) */
6393
6579
  /* 调色板全部在 style.css(html[data-skin="X"],每套含夜间/日间两版变量);这里只放顺序
6394
6580
  * 与文案。卡片预览色块用 skinPalette 从 CSS 变量实时取值,不在 JS 里重复写色值——
@@ -6796,7 +6982,7 @@ function updateChip(c) {
6796
6982
  async function autoCheckUpdates() {
6797
6983
  if (Date.now() - (S.updateCheckAt || 0) < 5 * 60 * 1000) return;
6798
6984
  S.updateCheckAt = Date.now();
6799
- try { await api("/api/catalog/check-updates", { method: "POST" }); } catch (e) { /* 忽略 */ }
6985
+ try { await api("/api/catalog/check-updates", { method: "POST", busy: false }); } catch (e) { /* 忽略 */ }
6800
6986
  poll();
6801
6987
  }
6802
6988
 
@@ -6954,7 +7140,7 @@ async function autoRebindSoon() {
6954
7140
  if (!act) continue;
6955
7141
  const b = (S.bindings || {})[c.id] || {};
6956
7142
  try {
6957
- await api("/api/models/binding", { method: "POST", body: JSON.stringify({
7143
+ await api("/api/models/binding", { method: "POST", busy: false, body: JSON.stringify({
6958
7144
  agent_id: c.id, provider_id: act[0].p,
6959
7145
  chain: act.map((x) => ({ provider_id: x.p, model: x.m })),
6960
7146
  difficulty_routing: !!b.difficulty_routing }) });
@@ -8882,7 +9068,7 @@ function suLastUpgradeRun() {
8882
9068
 
8883
9069
  async function loadSelfupdate(force) {
8884
9070
  try {
8885
- SU = await api("/api/selfupdate" + (force ? "?force=1" : ""));
9071
+ SU = await api("/api/selfupdate" + (force ? "?force=1" : ""), { busy: !!force });
8886
9072
  } catch (e) { SU = null; }
8887
9073
  renderSu();
8888
9074
  maybeWhatsnew();
@@ -9198,6 +9384,7 @@ async function savePetSkin(skin) {
9198
9384
  async function exportDiagBundle() {
9199
9385
  const b = $("diag-export");
9200
9386
  if (b) b.disabled = true;
9387
+ const busyToken = requestBusyStart({ method: "GET", busy: true, busyElement: b });
9201
9388
  try {
9202
9389
  const r = await fetch("/api/diagnostics/bundle", { headers: authHeaders() });
9203
9390
  if (!r.ok) throw new Error("HTTP " + r.status);
@@ -9213,6 +9400,7 @@ async function exportDiagBundle() {
9213
9400
  } catch (e) {
9214
9401
  toast(t("诊断包导出失败:") + e.message, true);
9215
9402
  } finally {
9403
+ requestBusyEnd(busyToken);
9216
9404
  if (b) b.disabled = false;
9217
9405
  }
9218
9406
  }
@@ -9220,7 +9408,7 @@ async function exportDiagBundle() {
9220
9408
  /* 一键反馈 Issue:拉脱敏摘要 → 预填 GitHub Issue 新建页(用户亲手提交,不自动回传) */
9221
9409
  async function reportIssue() {
9222
9410
  try {
9223
- const s = await api("/api/diagnostics/issue-summary");
9411
+ const s = await api("/api/diagnostics/issue-summary", { busy: true });
9224
9412
  const url = "https://github.com/Vercel-By-WXP/CodeBee/issues/new" +
9225
9413
  "?title=" + encodeURIComponent(s.title || "") +
9226
9414
  "&body=" + encodeURIComponent(s.body || "");
@@ -9231,6 +9419,64 @@ async function reportIssue() {
9231
9419
  }
9232
9420
  }
9233
9421
 
9422
+ /* 端口占用诊断(借鉴 leftopen):谁占着端口、属于哪个项目;可温和关闭 */
9423
+ async function scanPorts() {
9424
+ const box = $("ports-list");
9425
+ if (!box) return;
9426
+ const b = $("ports-scan");
9427
+ if (b) b.disabled = true;
9428
+ try {
9429
+ const r = await api("/api/ports");
9430
+ const ports = r.ports || [];
9431
+ if (!ports.length) { box.innerHTML = '<span class="hint">' + esc(t("没有发现监听端口")) + "</span>"; return; }
9432
+ box.innerHTML = "";
9433
+ const tbl = document.createElement("table");
9434
+ tbl.style.width = "100%";
9435
+ tbl.style.fontSize = "12px";
9436
+ tbl.style.borderCollapse = "collapse";
9437
+ const th = (s) => '<th style="text-align:left;padding:4px 8px;opacity:.7">' + esc(s) + "</th>";
9438
+ tbl.innerHTML = "<thead><tr>" + th(t("端口")) + th(t("进程")) + th(t("项目")) + th("") + "</tr></thead>";
9439
+ const tb = document.createElement("tbody");
9440
+ for (const p of ports) {
9441
+ const tr = document.createElement("tr");
9442
+ const td = (s) => '<td style="padding:4px 8px;border-top:1px solid var(--border)">' + s + "</td>";
9443
+ tr.innerHTML =
9444
+ td(p.port + (p.local_only ? ' <span class="pill">' + esc(t("仅本机")) + "</span>" : "") + (p.self ? ' <span class="pill">' + esc(t("本服务")) + "</span>" : "")) +
9445
+ td(esc((p.process || "?") + (p.pid ? " · pid " + p.pid : ""))) +
9446
+ td(p.project ? esc(p.project) : "-") +
9447
+ "<td></td>";
9448
+ const act = tr.lastElementChild;
9449
+ if (!p.self && p.pid) {
9450
+ const btn = document.createElement("button");
9451
+ btn.className = "ghost small";
9452
+ btn.textContent = t("关闭");
9453
+ btn.onclick = () => closePort(p.port);
9454
+ act.appendChild(btn);
9455
+ }
9456
+ tb.appendChild(tr);
9457
+ }
9458
+ tbl.appendChild(tb);
9459
+ box.appendChild(tbl);
9460
+ } catch (e) {
9461
+ box.innerHTML = '<span class="hint">' + esc(t("扫描失败:") + (e.message || e)) + "</span>";
9462
+ } finally {
9463
+ if (b) b.disabled = false;
9464
+ }
9465
+ }
9466
+
9467
+ async function closePort(port) {
9468
+ if (!confirm(t("向占用端口 %1 的进程发送温和关闭信号?(系统进程会被拒绝)").replace("%1", port))) return;
9469
+ try {
9470
+ const r = await api("/api/ports/close", { method: "POST", body: JSON.stringify({ port }) });
9471
+ toast(r.message || (r.ok ? t("已发送关闭信号") : t("未能关闭")), !r.ok);
9472
+ scanPorts();
9473
+ } catch (e) {
9474
+ toast(e.message || t("关闭失败"), true);
9475
+ }
9476
+ }
9477
+
9478
+
9479
+
9234
9480
  async function suCheck() {
9235
9481
  const b = $("su-check");
9236
9482
  if (b) b.disabled = true;
package/app/ui/i18n.js CHANGED
@@ -1273,6 +1273,7 @@
1273
1273
  "安装中…": "Installing…",
1274
1274
  "正在下载安装,插件包较大时需要约一分钟…": "Downloading and installing — large plugin packages can take about a minute…",
1275
1275
  "请求超时,请重试或检查网络": "Request timed out — try again or check your network",
1276
+ "操作处理中": "Working on it",
1276
1277
  "该插件含脚本/钩子/MCP 组件,仅支持纯技能类插件": "Contains scripts/hooks/MCP components — only pure skill plugins are supported",
1277
1278
  " 个供应商(google 等协议)仅登记,不支持注入 CLI,未出现在上面的下拉中。": " provider(s) (google etc.) are registry-only, can't inject into CLIs, and are hidden from the dropdown above.",
1278
1279
  " 个降级备选)。": " fallback(s)).",
@@ -1866,6 +1867,23 @@
1866
1867
  "一键反馈 Issue": "Report Issue",
1867
1868
  "已打开 GitHub 反馈页,内容已预填,可直接提交(也可附上诊断包 zip)": "GitHub issue page opened with the report pre-filled — submit it, and feel free to attach the diagnostics zip",
1868
1869
  "反馈摘要生成失败:": "Failed to build issue summary: ",
1870
+ "端口占用": "Port Usage",
1871
+ "扫描本机端口": "Scan local ports",
1872
+ "谁占着端口、属于哪个项目一目了然;「仅本机」的端口外网不可达。关闭只发温和信号,系统进程与本服务自身拒绝关闭。": "See at a glance which process holds each port and which project it belongs to; \"local only\" ports are unreachable from outside. Close sends a gentle termination signal only — system processes and this service itself are refused.",
1873
+ "端口": "Port",
1874
+ "进程": "Process",
1875
+ "项目": "Project",
1876
+ "仅本机": "local only",
1877
+ "本服务": "this service",
1878
+ "关闭": "Close",
1879
+ "没有发现监听端口": "No listening ports found",
1880
+ "扫描失败:": "Scan failed: ",
1881
+ "向占用端口 %1 的进程发送温和关闭信号?(系统进程会被拒绝)": "Send a gentle close signal to the process holding port %1? (system processes are refused)",
1882
+ "已发送关闭信号": "Close signal sent",
1883
+ "未能关闭": "Could not close",
1884
+ "关闭失败": "Close failed",
1885
+
1886
+
1869
1887
 
1870
1888
  // —— 禅道 Bug 自动修复 ——
1871
1889
  "禅道": "ZenTao",
@@ -2260,6 +2278,23 @@
2260
2278
  "旧版浏览器缓存残留": "Legacy browser cache remains",
2261
2279
  "历史备份包": "Old backup archives",
2262
2280
  "服务日志瘦身": "Service log trim",
2281
+ "任务画像与调度": "Task profile & routing",
2282
+ "通用能力": "general capabilities",
2283
+ "未选定": "Not selected",
2284
+ "执行": "Implement",
2285
+ "降级:": "Fallback: ",
2286
+ "查看候选评分": "View candidate scores",
2287
+ "需要:": "Needs: ",
2288
+ "简单": "Easy",
2289
+ "标准": "Standard",
2290
+ "困难": "Hard",
2291
+ "编码": "Coding",
2292
+ "写作": "Writing",
2293
+ "推理": "Reasoning",
2294
+ "视觉": "Vision",
2295
+ "文件读写": "File access",
2296
+ "长上下文": "Long context",
2297
+ "连续性": "Continuity",
2263
2298
  "发布浏览器缓存": "Publishing browser cache",
2264
2299
  "保留运行记录与结果报告,只删过程中的原始日志": "Keeps run records and result reports; only raw process logs are deleted",
2265
2300
  "发布过程留档截图,超期即清": "Screenshots kept as publishing evidence, removed once expired",