codebee 0.1.30 → 0.1.32

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
@@ -2,7 +2,12 @@
2
2
  "use strict";
3
3
 
4
4
  // 桌宠通过窗口标题里的端口识别并复用当前 CodeBee 实例。
5
- document.title = "CodeBee [" + (location.port || (location.protocol === "https:" ? "443" : "80")) + "]";
5
+ function codebeeDocumentTitle(lang) {
6
+ const brand = lang === "en" ? "CodeBee · Multi-agent Orchestrator" : "CodeBee · 多智能体编排台";
7
+ const port = location.port || (location.protocol === "https:" ? "443" : "80");
8
+ return brand + " [" + port + "]";
9
+ }
10
+ document.title = codebeeDocumentTitle((localStorage.getItem("orch.lang") || "zh").toLowerCase());
6
11
 
7
12
  const $ = (id) => document.getElementById(id);
8
13
  const S = { state: null, catalog: null, catSig: "", providers: null, bindings: null, modelsSig: "", bindSig: "", tab: "tasks", detailRunId: null, pollTimer: null, showArchived: false, /* 会话内开关:每次加载默认隐藏已归档、图标不选中(不持久化,见 btn-side-arch) */ selProvs: {}, selModels: {}, selRuns: {}, bindSel: {}, catalogChecking: false, updateCheckAt: 0, control: null, sseLive: false, es: null, flows: null, orch: null, skills: null, orchSig: "", settings: null, sessionAgents: new Set(), atts: [], gitInfo: null, gitWb: null, gitWbKey: "", gitWbAt: 0, gitWbBusy: false, creatingTask: false, inspKey: null, inspData: null, inspSig: "", inspAt: 0, inspTab: "git", inspAutoSig: "", rdTab: null, rdTabSig: "", rdTabPin: false, _rdCtx: {}, lastPrefs: null };
@@ -272,8 +277,8 @@ function cmpGreeting() {
272
277
  : "晚上好呀,有什么想让我帮忙的吗");
273
278
  }
274
279
 
275
- /* 开跑前成本预估(借鉴 omnigent 的 pre-run estimate):读台账同类任务给量级参考。
276
- * 无样本/请求失败都保持隐藏;快速切类型用序号丢弃过期响应。 */
280
+ /* 开跑前成本与耗时预估(借鉴 omnigent 的 pre-run estimate):优先读同类台账,
281
+ * 无历史时显示流程基线;快速切类型用序号丢弃过期响应。 */
277
282
  let _estSeq = 0;
278
283
  async function refreshEstimate() {
279
284
  const el = $("cmp-estimate");
@@ -281,13 +286,26 @@ async function refreshEstimate() {
281
286
  if (!el) return;
282
287
  const seq = ++_estSeq;
283
288
  try {
284
- const d = await api("/api/usage/estimate?type=" + encodeURIComponent(flowId) + "&days=90");
289
+ const mode = ($("f-mode") || {}).value || "auto";
290
+ const thinking = ($("f-thinking") || {}).value || "standard";
291
+ const rounds = ($("f-rounds") || {}).value || "";
292
+ const query = new URLSearchParams({ type: flowId, days: "90", mode, thinking });
293
+ if (rounds) query.set("rounds", rounds);
294
+ const d = await api("/api/usage/estimate?" + query.toString());
285
295
  if (seq !== _estSeq) return;
286
- if (!d || !(d.samples > 0)) { el.classList.add("hidden"); el.textContent = ""; return; }
287
- const tk = "≈" + fmtTok(d.median_tokens) + " tokens";
288
- const tail = "(" + t("近{0}天 · {1}次同类", d.days || 90, d.samples) + ")";
289
- const cost = (d.median_cost_usd || 0) > 0 ? " · ≈$" + (d.median_cost_usd).toFixed(2) : "";
290
- el.textContent = t("预估:≈{0}{1}", tk, cost) + tail;
296
+ if (!d || !(d.estimated_duration_s > 0)) {
297
+ el.classList.add("hidden"); el.textContent = ""; return;
298
+ }
299
+ const duration = chatDurTxt(d.estimated_duration_s);
300
+ const p90 = chatDurTxt(d.p90_duration_s || d.estimated_duration_s);
301
+ const source = d.samples > 0
302
+ ? t("近{0}天 · {1}次同类", d.days || 90, d.samples)
303
+ : t("暂无同类历史,按流程基线估算");
304
+ const tokenPart = d.samples > 0 && d.median_tokens != null
305
+ ? " · ≈" + fmtTok(d.median_tokens) + " tokens" : "";
306
+ const cost = (d.median_cost_usd || 0) > 0 ? " · ≈$" + Number(d.median_cost_usd).toFixed(2) : "";
307
+ el.textContent = t("预计完成约 {0},保守不超过 {1}", duration, p90) +
308
+ tokenPart + cost + "(" + source + ")";
291
309
  el.classList.remove("hidden");
292
310
  } catch (e) {
293
311
  if (seq === _estSeq) { el.classList.add("hidden"); el.textContent = ""; }
@@ -4032,12 +4050,14 @@ async function renderRunDetail() {
4032
4050
  '<span class="stat">' + t("创建 ") + '<b>' + esc(run.created_at) + "</b></span>" +
4033
4051
  '<span class="stat">' + t("成本 ") + '<b>$' + Number(run.cost_usd || 0).toFixed(3) + "</b></span>" +
4034
4052
  '<span class="stat">tokens <b>' + (run.tokens || 0) + "</b></span>" +
4035
- (run.mode ? '<span class="stat">' + t("模式 ") + '<b>' + (run.mode === "auto" ? t("智能") : t("手动")) + "</b></span>" : "") +
4053
+ (run.mode ? '<span class="stat">' + t("模式 ") + '<b>' + ({auto: t("自动"), fast: t("快速"), expert: t("专家"), manual: t("手动")}[run.mode] || esc(run.mode)) + "</b></span>" : "") +
4054
+ (runEtaText(run) ? '<span class="stat eta">' + esc(runEtaText(run)) + "</span>" : "") +
4036
4055
  (run.error ? '<span class="stat err">' + errTag(run.error) + esc(run.error.slice(0, 200)) + "</span>" : "") +
4037
4056
  '<span class="stat tasksum hidden" id="rd-meta-task"></span>';
4038
4057
  if (S.detailSide) fillMetaTask(S.detailSide.stats || {}); // 缓存命中:轮询重画不闪丢累计组
4039
4058
  renderPlan(run);
4040
4059
  renderRouting(run);
4060
+ renderChapterScores(run);
4041
4061
  // 详情页优先展示最近一步,排查运行中的任务时无需滚到底部;
4042
4062
  // 蜂巢泳道仍按原始流程顺序呈现,避免破坏阶段语义。
4043
4063
  $("rd-steps").innerHTML = (run.steps || []).slice().reverse().map((s) =>
@@ -5665,6 +5685,38 @@ function stepsMatch(runs, current) {
5665
5685
  (r.steps || []).some((s) => s.log === current.rel));
5666
5686
  }
5667
5687
 
5688
+ /* 连载章节评审卡:每章均分/是否达标/字数 + 逐项目标审稿结果(event_check,
5689
+ * 借鉴 AI-Novel-Writer:大纲要点逐项判定+正文证据)。非连载 run 无
5690
+ * chapter_scores 保持隐藏。 */
5691
+ function renderChapterScores(run) {
5692
+ const box = $("rd-chapters");
5693
+ if (!box) return;
5694
+ const cs = run && run.chapter_scores;
5695
+ if (!Array.isArray(cs) || !cs.length) { box.classList.add("hidden"); return; }
5696
+ box.classList.remove("hidden");
5697
+ box.innerHTML = '<h3 class="sec-title">' + t("章节评审") +
5698
+ '<span class="tag">' + cs.filter((c) => c.passed).length + "/" + cs.length + " " + t("达标") + "</span></h3>" +
5699
+ cs.map((c) => {
5700
+ const means = c.means || {};
5701
+ const dims = Object.keys(means).map((d) =>
5702
+ '<span class="cs-dim' + (means[d] >= 7 ? "" : " low") + '">' + esc(d) + " " + means[d] + "</span>").join("");
5703
+ const ev = (c.event_check || []).map((line) => {
5704
+ const ok = /已完成/.test(line), bad = /未完成/.test(line);
5705
+ return '<div class="cs-ev' + (ok ? " ok" : bad ? " bad" : "") + '">' + esc(line) + "</div>";
5706
+ }).join("");
5707
+ return '<div class="cs-row' + (c.passed ? "" : " fail") + '">' +
5708
+ '<div class="cs-head"><b>' + esc(t("第") + " " + c.chapter + " " + t("章")) + '</b>' +
5709
+ '<span class="cs-title">' + esc(c.title || "") + "</span>" +
5710
+ '<span class="tag">' + (c.passed ? t("达标") : t("未达标")) + "</span>" +
5711
+ (c.reused ? '<span class="tag">' + t("沿用") + "</span>" : "") +
5712
+ '<span class="cs-meta">' + Number(c.words || 0) + t(" 字") +
5713
+ (c.rounds > 1 ? " · " + c.rounds + t(" 轮") : "") + "</span></div>" +
5714
+ (dims ? '<div class="cs-dims">' + dims + "</div>" : "") +
5715
+ (ev ? '<div class="cs-evs"><span class="hint">' + t("逐项目标核对:") + "</span>" + ev + "</div>" : "") +
5716
+ "</div>";
5717
+ }).join("");
5718
+ }
5719
+
5668
5720
  function renderPlan(run) {
5669
5721
  const box = $("rd-plan");
5670
5722
  const plan = run.plan;
@@ -6513,6 +6565,29 @@ function runDurationSeconds(run) {
6513
6565
  return Number.isFinite(a) && Number.isFinite(b) && b >= a ? (b - a) / 1000 : null;
6514
6566
  }
6515
6567
 
6568
+ function runEtaText(run) {
6569
+ if (!run || !Number.isFinite(Number(run.estimated_duration_s))) return "";
6570
+ const estimate = Math.max(1, Number(run.estimated_duration_s));
6571
+ const p90 = Math.max(estimate, Number(run.estimated_p90_s) || estimate);
6572
+ const active = run.status === "running" || run.status === "queued";
6573
+ if (!active) return t("预计完成约 {0}", chatDurTxt(Math.round(estimate)));
6574
+ if (run.status === "queued" || !run.started_at) {
6575
+ return t("预计完成约 {0},保守不超过 {1}",
6576
+ chatDurTxt(Math.round(estimate)), chatDurTxt(Math.round(p90)));
6577
+ }
6578
+ const started = Date.parse(String(run.started_at).replace(" ", "T"));
6579
+ if (!Number.isFinite(started)) return "";
6580
+ const elapsed = Math.max(0, (Date.now() - started) / 1000);
6581
+ if (elapsed < estimate) {
6582
+ return t("预计剩余约 {0}", chatDurTxt(Math.max(1, Math.round(estimate - elapsed))));
6583
+ }
6584
+ if (elapsed < p90) {
6585
+ return t("已超过常规预估,仍在保守区间内(约剩 {0})",
6586
+ chatDurTxt(Math.max(1, Math.round(p90 - elapsed))));
6587
+ }
6588
+ return t("已超过保守预估,正在继续执行,可查看步骤日志");
6589
+ }
6590
+
6516
6591
  function runOutcomeSummary(run) {
6517
6592
  if (!run) return "";
6518
6593
  const last = (run.steps || []).slice().reverse().find((s) => s.output || s.summary);
@@ -6579,6 +6654,7 @@ function renderDetailOverview(run, runs, task) {
6579
6654
  const lastText = last ? String(last.summary || last.output || last.note || "").replace(/\s+/g, " ").trim().slice(0, 240) : "";
6580
6655
  const executor = run.executor || run.implementer || (last && (last.agent_label || last.agent)) || "";
6581
6656
  const duration = runDurationSeconds(run);
6657
+ const etaText = runEtaText(run);
6582
6658
  const goal = (task && task.goal) || run.goal || run.title || "";
6583
6659
  const actions = [];
6584
6660
  if (total) actions.push('<button type="button" class="ghost" data-overview-tab="steps">' + esc(t("查看步骤")) + "</button>");
@@ -6599,6 +6675,7 @@ function renderDetailOverview(run, runs, task) {
6599
6675
  '<span><b>' + settled + "/" + total + '</b> ' + esc(t("步骤已完成")) + '</span>' +
6600
6676
  (executor ? '<span>' + esc(t("执行者")) + ' <b>' + esc(executor) + '</b></span>' : "") +
6601
6677
  (duration != null ? '<span>' + esc(t("耗时")) + ' <b>' + esc(chatDurTxt(duration < 1 ? Number(duration.toFixed(1)) : Math.round(duration))) + '</b></span>' : "") +
6678
+ (etaText ? '<span class="rd-eta">' + esc(etaText) + '</span>' : "") +
6602
6679
  '</div>' +
6603
6680
  (lastText ? '<div class="rd-overview-last"><b>' + esc(t("最近一步")) + '</b><span>' + esc(lastText) + '</span></div>' : "") +
6604
6681
  (actions.length ? '<div class="rd-overview-actions">' + actions.join("") + '</div>' : "");
@@ -9857,7 +9934,7 @@ async function suStartupCheck() {
9857
9934
  }
9858
9935
 
9859
9936
  /* ---------------------------------------------------------- 页签 & 初始化 */
9860
- const TAB_TITLES = { tasks: "任务", runs: "运行记录", automation: "自动化", zentao: "禅道 Bug 自动修复", usage: "用量统计", agents: "本机智能体", models: "模型接入", bindings: "模型调度(可选)", skills: "经验库", knowledge: "知识库", market: "插件市场", orch: "编排设置", data: "数据与备份", appearance: "皮肤", about: "关于与更新" };
9937
+ const TAB_TITLES = { tasks: "任务", runs: "运行记录", automation: "自动化", zentao: "禅道 Bug 自动修复", __wxdigest: "群摘要", usage: "用量统计", agents: "本机智能体", models: "模型接入", bindings: "模型调度(可选)", skills: "经验库", knowledge: "知识库", market: "插件市场", orch: "编排设置", data: "数据与备份", appearance: "皮肤", about: "关于与更新" };
9861
9938
  const SET_TABS = new Set(Object.keys(TAB_TITLES)); // 设置导航里的子页(__phone 是弹框,不算)
9862
9939
 
9863
9940
  function tabTitle(name) {
@@ -10013,7 +10090,7 @@ function setLangBtn(lang) {
10013
10090
  S.catSig = ""; S.modelsSig = ""; S.bindSig = ""; S.orchSig = ""; S.sideSig = ""; S.taskSig = ""; S.mgmt = {};
10014
10091
  syncLangMode();
10015
10092
  // 浏览器标签标题
10016
- document.title = (lang === "en" ? "CodeBee · Multi-agent Orchestrator" : "CodeBee · 多智能体编排台");
10093
+ document.title = codebeeDocumentTitle(lang);
10017
10094
  applyI18n();
10018
10095
  render();
10019
10096
  cmpGreeting(); // 问候语跟随语言重算
@@ -10696,11 +10773,6 @@ function collapseDrawerIfMobile() {
10696
10773
  function switchTab(name) {
10697
10774
  if (name === "__phone") { openPhoneConnect(); return; } // 手机连接是弹框,不切页
10698
10775
  if (name === "__guide") { welcomeOpen(); return; } // 帮助中心是弹层,不切页(设置导航「软件」组)
10699
- if (name === "__wxdigest") { // 群聊汇报沿用全局蜜蜂面板
10700
- beeRefresh().finally(() => beeToggle(true));
10701
- collapseDrawerIfMobile();
10702
- return;
10703
- }
10704
10776
  // 导航收进「设置」:进设置后左栏整体换成设置导航,内容铺满
10705
10777
  S.tab = name;
10706
10778
  if (SET_TABS.has(name)) localStorage.setItem("orch.setTab", name);
@@ -10719,6 +10791,7 @@ function switchTab(name) {
10719
10791
  if (name === "automation") { loadAutomation(); startAutoPoll(); } // 进自动化页:拉取 + 页面可见时每 8s 轮询
10720
10792
  else stopAutoPoll(); // 离开自动化页(或切到别的子页)即停表
10721
10793
  if (name === "zentao") loadZentao(); // 进禅道页:拉配置与修复记录回填表单
10794
+ if (name === "__wxdigest") { beeRefresh(); beeMarkSeen(); } // 进群摘要页:拉取视图并清未读
10722
10795
  if (name === "market") loadMarket(); // 进插件市场页拉取目录
10723
10796
  if (name === "data") loadDataPage(); // 进数据与备份页:清理配置/状态/可清理预估
10724
10797
  if (name === "usage") { syncUsageRange(); loadUsage(); } // 进用量页:对齐范围选中态并拉取
@@ -11186,7 +11259,10 @@ document.addEventListener("DOMContentLoaded", () => {
11186
11259
  });
11187
11260
  $("f-mode").addEventListener("change", () => {
11188
11261
  $("f-manual-only").classList.toggle("hidden", $("f-mode").value !== "manual");
11262
+ refreshEstimate();
11189
11263
  });
11264
+ $("f-thinking").addEventListener("change", refreshEstimate);
11265
+ $("f-rounds").addEventListener("change", refreshEstimate);
11190
11266
  $("f-direct-provider").addEventListener("change", renderDirectModelPicker);
11191
11267
  $("f-type").dispatchEvent(new Event("change"));
11192
11268
  cmpGreeting(); // 问候语按时段刷新(切语言/回任务页也会重算)
@@ -11413,46 +11489,20 @@ function bindCmdK() {
11413
11489
  });
11414
11490
  }
11415
11491
 
11416
- /* ------------------------------------------------ 群摘要蜜蜂坞(core/wxdigest.py)
11417
- * 右下角悬浮蜜蜂:未读摘要徽章+气泡;面板看各群摘要、配置监控文件夹。
11418
- * 桌面蜜蜂 pet.py 的网页侧搭档;入口始终可见,未配置时也能直接打开设置。
11419
- * 60s 轮询 /api/wxdigest;打开面板即 POST seen 清零未读。 */
11420
- const Bee = { view: null, lastUnseen: 0, bubT: 0, selGroups: null };
11492
+ /* ------------------------------------------------ 群摘要页(core/wxdigest.py)
11493
+ * 设置 → 群摘要:配置监控文件夹/微信直连 + 各群摘要列表(替代原右下角悬浮
11494
+ * 蜜蜂坞——用户拍板去掉悬浮组件,配置与总结统一进页面)。桌面蜜蜂 pet.py
11495
+ * 右键「群摘要」直达本页。进页/60s 轮询 /api/wxdigest;进页 POST seen 清未读。 */
11496
+ const Bee = { view: null, lastUnseen: 0, bubT: 0, selGroups: null, pyDirty: false };
11421
11497
 
11422
11498
  function beeShow(el, on) { if (el) el.classList.toggle("hidden", !on); }
11423
- function beePanelOpen() { const p = $("bee-panel"); return !!p && !p.classList.contains("hidden"); }
11424
11499
 
11425
11500
  async function beeRefresh() {
11426
11501
  let v;
11427
11502
  try { v = await api("/api/wxdigest"); } catch (e) { return; } // 静默:不打扰主流程
11428
11503
  Bee.view = v;
11429
- beeShow($("bee-dock"), true);
11430
- beeShow($("bee-badge"), v.unseen > 0);
11431
- if (v.unseen > 0) $("bee-badge").textContent = v.unseen > 99 ? "99+" : String(v.unseen);
11432
- if (v.unseen > Bee.lastUnseen && !beePanelOpen()) { // 新摘要到:气泡 6s
11433
- const b = $("bee-bubble");
11434
- const names = [...new Set((v.digests || []).slice(0, v.unseen).map((d) => d.group))];
11435
- b.textContent = t("嗡!新群摘要好了:") + (names.join(t("、")) || "…");
11436
- beeShow(b, true);
11437
- clearTimeout(Bee.bubT);
11438
- Bee.bubT = setTimeout(() => beeShow(b, false), 6000);
11439
- }
11440
11504
  Bee.lastUnseen = v.unseen;
11441
- if (beePanelOpen()) beeRenderPanel();
11442
- }
11443
-
11444
- function beeToggle(force) {
11445
- const p = $("bee-panel");
11446
- if (!p) return;
11447
- const open = force !== undefined ? force : p.classList.contains("hidden");
11448
- beeShow(p, open);
11449
- if (open) {
11450
- const v = Bee.view || {};
11451
- const hasData = (v.digests || []).length > 0;
11452
- beeShow($("bee-cfg"), !hasData); // 没数据时首开直落配置(可发现性)
11453
- beeRenderPanel();
11454
- beeMarkSeen();
11455
- }
11505
+ beeRenderPanel();
11456
11506
  }
11457
11507
 
11458
11508
  async function beeMarkSeen() {
@@ -11460,7 +11510,6 @@ async function beeMarkSeen() {
11460
11510
  const r = await api("/api/wxdigest/seen", { method: "POST", body: "{}" });
11461
11511
  Bee.view = r.view;
11462
11512
  Bee.lastUnseen = 0;
11463
- beeShow($("bee-badge"), false);
11464
11513
  beeRenderPanel();
11465
11514
  } catch (e) { /* ignore */ }
11466
11515
  }
@@ -11472,7 +11521,10 @@ function beeRenderPanel() {
11472
11521
  $("bee-enabled").checked = !!cfg.enabled;
11473
11522
  $("bee-reader").checked = !!cfg.reader_enabled;
11474
11523
  beeShow($("bee-reader-box"), !!cfg.reader_enabled);
11475
- if (document.activeElement !== $("bee-reader-py")) $("bee-reader-py").value = cfg.reader_python || "";
11524
+ // 用户手动改过路径(选候选/输入)后轮询不再回填覆盖——否则 60s 一冲,
11525
+ // 选择根本存不进去(「选不中 Python」根因之一)
11526
+ if (!Bee.pyDirty && document.activeElement !== $("bee-reader-py"))
11527
+ $("bee-reader-py").value = cfg.reader_python || "";
11476
11528
  if (!Bee.selGroups) Bee.selGroups = (cfg.groups || []).slice();
11477
11529
  const n = Bee.selGroups.length;
11478
11530
  $("bee-groups-btn").textContent = t("选择要监听的群({0})", n);
@@ -11585,6 +11637,7 @@ async function beeSave() {
11585
11637
  }) });
11586
11638
  if (Bee.view) Bee.view.config = r.config;
11587
11639
  Bee.selGroups = null; // 下次渲染用保存后的值重建勾选态
11640
+ Bee.pyDirty = false; // 保存成功:服务端值与所见一致,恢复轮询回填
11588
11641
  toast(t("群摘要设置已保存"));
11589
11642
  beeRefresh();
11590
11643
  } catch (e) { toast(t("保存失败:") + (e.message || e), true); }
@@ -11606,13 +11659,7 @@ async function beeScan() {
11606
11659
 
11607
11660
  (function () {
11608
11661
  const boot = () => {
11609
- if (!$("bee-fab")) return;
11610
- $("bee-fab").addEventListener("click", () => beeToggle());
11611
- $("bee-x").addEventListener("click", () => beeToggle(false));
11612
- $("bee-cfg-btn").addEventListener("click", () => {
11613
- const c = $("bee-cfg");
11614
- beeShow(c, c.classList.contains("hidden"));
11615
- });
11662
+ if (!$("bee-save")) return;
11616
11663
  $("bee-save").addEventListener("click", beeSave);
11617
11664
  $("bee-scan-btn").addEventListener("click", beeScan);
11618
11665
  $("bee-dir-pick").addEventListener("click", () => window.pickFolder("bee-dir", true));
@@ -11632,11 +11679,13 @@ async function beeScan() {
11632
11679
  // 别手填解释器路径、别自己开 conda 装依赖)
11633
11680
  $("bee-py-scan").addEventListener("click", beePyScan);
11634
11681
  $("bee-py-install").addEventListener("click", beePyInstall);
11682
+ // 用户手动改过路径(选候选/输入)后置脏:轮询不再回填覆盖
11683
+ $("bee-reader-py").addEventListener("input", () => { Bee.pyDirty = true; });
11635
11684
  beeRefresh();
11636
11685
  setInterval(beeRefresh, 60000);
11637
- // 桌宠右键「群摘要」直达:URL 带 #bee=1 时自动展开蜜蜂坞面板
11638
- if (/bee=1/.test(location.hash)) {
11639
- setTimeout(() => beeToggle(true), 800);
11686
+ // 桌宠右键「群摘要」直达:URL 带 #goto=__wxdigest 时切到群摘要子页
11687
+ if (/goto=__wxdigest/.test(location.hash)) {
11688
+ setTimeout(() => switchTab("__wxdigest"), 800);
11640
11689
  history.replaceState(null, "", location.pathname + location.search);
11641
11690
  }
11642
11691
  };
package/app/ui/i18n.js CHANGED
@@ -607,6 +607,15 @@
607
607
  "重命名任务": "Rename task",
608
608
  "↻ 继续任务": "↻ Resume task",
609
609
  "↻ 重写未达标章": "↻ Rewrite unqualified chapters",
610
+ "章节评审": "Chapter Reviews",
611
+ "达标": "passed",
612
+ "未达标": "missed",
613
+ "沿用": "reused",
614
+ "逐项目标核对:": "Beat-by-beat check: ",
615
+ "第": "Ch.",
616
+ "章": "",
617
+ " 字": " words",
618
+ " 轮": " rounds",
610
619
  "只重写上一遍未过线的章节,已过线章节沿用成稿与评分": "Rewrites only the chapters that missed the threshold last run; passing chapters keep their drafts and scores.",
611
620
  "再跑一次这个任务;连载任务会断点续跑,已完成章节不重写": "Run this task again; serial novels resume from the breakpoint — finished chapters are kept.",
612
621
  "✎ 编辑重试": "✎ Edit & retry",
@@ -766,6 +775,12 @@
766
775
  "常用类型": "Common types",
767
776
  "预估:≈{0}{1}": "Est: ≈{0}{1}",
768
777
  "近{0}天 · {1}次同类": "last {0}d · {1} similar runs",
778
+ "暂无同类历史,按流程基线估算": "No similar history; estimated from the workflow baseline",
779
+ "预计完成约 {0}": "Estimated completion: about {0}",
780
+ "预计完成约 {0},保守不超过 {1}": "Estimated completion: about {0}; conservative bound {1}",
781
+ "预计剩余约 {0}": "About {0} remaining",
782
+ "已超过常规预估,仍在保守区间内(约剩 {0})": "Past the usual estimate but within the conservative bound (about {0} remaining)",
783
+ "已超过保守预估,正在继续执行,可查看步骤日志": "Past the conservative estimate; still running. Check step logs for progress",
769
784
  "候选并发数(单稿赛马,1=关)": "Best-of-N candidates (1=off)",
770
785
  "同时起草多份候选稿,终审择优并吸收落选稿优点;每多一路多一份起草开销": "Draft several candidates in parallel; a final selector picks the best and harvests the losers' strengths. Each extra candidate costs one more draft",
771
786
  "建 PR": "Create PR",