codebee 0.1.29 → 0.1.31

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
@@ -1,6 +1,14 @@
1
1
  /* CodeBee 前端(无依赖;状态走 SSE 实时推送,断线自动降级轮询) */
2
2
  "use strict";
3
3
 
4
+ // 桌宠通过窗口标题里的端口识别并复用当前 CodeBee 实例。
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());
11
+
4
12
  const $ = (id) => document.getElementById(id);
5
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 };
6
14
 
@@ -155,6 +163,8 @@ function onTypeChange() {
155
163
  // 直连:验证命令与评审参数都不适用,两块一起收起(目标+附件即全部输入)
156
164
  if (codeOnly) codeOnly.classList.toggle("hidden", isReview || isDirect);
157
165
  if (reviewOnly) reviewOnly.classList.toggle("hidden", !isReview);
166
+ if ($("f-direct-model")) $("f-direct-model").classList.toggle("hidden", !isDirect);
167
+ if (isDirect) renderDirectModelPicker();
158
168
  const bibleCreate = $("bible-create-field");
159
169
  const isSerialReview = isReview && !!(flow && flow.serial);
160
170
  if (bibleCreate) bibleCreate.classList.toggle("hidden", !isSerialReview);
@@ -196,6 +206,46 @@ function onTypeChange() {
196
206
  refreshEstimate();
197
207
  }
198
208
 
209
+ function directProviders() {
210
+ return (S.providers || []).filter((p) => p.enabled !== false && p.api_key);
211
+ }
212
+
213
+ function renderDirectModelPicker() {
214
+ const ps = $("f-direct-provider"), ms = $("f-direct-model-name");
215
+ if (!ps || !ms) return;
216
+ const prev = ps.value;
217
+ const provs = directProviders();
218
+ ps.innerHTML = '<option value="">' + t("自动推荐") + '</option>' + provs.map((p) =>
219
+ '<option value="' + esc(p.id) + '">' + esc(p.name || p.id) + "</option>").join("");
220
+ if (prev && provs.some((p) => p.id === prev)) ps.value = prev;
221
+ const p = provs.find((x) => x.id === ps.value);
222
+ const oldModel = ms.value;
223
+ const names = p ? (p.models || []).filter((m) => m.enabled !== false && !m.hidden)
224
+ .map((m) => m.name).filter(Boolean) : [];
225
+ ms.innerHTML = '<option value="">' + t("随厂商推荐") + '</option>' + names.map((name) =>
226
+ '<option value="' + esc(name) + '">' + esc(name) + "</option>").join("");
227
+ if (oldModel && names.includes(oldModel)) ms.value = oldModel;
228
+ }
229
+
230
+ function recommendTaskType(goal) {
231
+ const s = String(goal || "").toLowerCase();
232
+ const rules = [
233
+ ["code", /(代码|bug|报错|修复|重构|接口|数据库|前端|后端|python|javascript|typescript|java|go\b|git)/i],
234
+ ["translation", /(翻译|译成|translate|translation|中译英|英译中)/i],
235
+ ["email", /(邮件|email|e-mail|回信|邀约信|商务函)/i],
236
+ ["weekly_report", /(周报|月报|述职|工作汇报|工作总结)/i],
237
+ ["video_script", /(短视频|口播|分镜|抖音|视频脚本)/i],
238
+ ["speech", /(演讲稿|发言稿|致辞|演讲)/i],
239
+ ["tech_proposal", /(技术方案|架构方案|选型方案|实施方案)/i],
240
+ ["research", /(调研报告|竞品调研|市场调研|深度研究)/i],
241
+ ["serial_novel", /(连载|续写.*章|网文|长篇小说)/i],
242
+ ["novel", /(小说|故事|短篇|章节)/i],
243
+ ["article", /(公众号|自媒体|文章|博客|小红书)/i],
244
+ ];
245
+ const hit = rules.find((x) => x[1].test(s));
246
+ return hit && flowById(hit[0]) ? hit[0] : "";
247
+ }
248
+
199
249
  /* 快捷类型 chips(Composer 框下方):内置流程一键选定,选中态跟 #f-type 走 */
200
250
  function renderQuickChips() {
201
251
  const box = $("cmp-quick");
@@ -227,8 +277,8 @@ function cmpGreeting() {
227
277
  : "晚上好呀,有什么想让我帮忙的吗");
228
278
  }
229
279
 
230
- /* 开跑前成本预估(借鉴 omnigent 的 pre-run estimate):读台账同类任务给量级参考。
231
- * 无样本/请求失败都保持隐藏;快速切类型用序号丢弃过期响应。 */
280
+ /* 开跑前成本与耗时预估(借鉴 omnigent 的 pre-run estimate):优先读同类台账,
281
+ * 无历史时显示流程基线;快速切类型用序号丢弃过期响应。 */
232
282
  let _estSeq = 0;
233
283
  async function refreshEstimate() {
234
284
  const el = $("cmp-estimate");
@@ -236,13 +286,26 @@ async function refreshEstimate() {
236
286
  if (!el) return;
237
287
  const seq = ++_estSeq;
238
288
  try {
239
- 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());
240
295
  if (seq !== _estSeq) return;
241
- if (!d || !(d.samples > 0)) { el.classList.add("hidden"); el.textContent = ""; return; }
242
- const tk = "≈" + fmtTok(d.median_tokens) + " tokens";
243
- const tail = "(" + t("近{0}天 · {1}次同类", d.days || 90, d.samples) + ")";
244
- const cost = (d.median_cost_usd || 0) > 0 ? " · ≈$" + (d.median_cost_usd).toFixed(2) : "";
245
- 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 + ")";
246
309
  el.classList.remove("hidden");
247
310
  } catch (e) {
248
311
  if (seq === _estSeq) { el.classList.add("hidden"); el.textContent = ""; }
@@ -919,6 +982,9 @@ async function poll() {
919
982
  S.providers = models.providers; S.bindings = models.bindings;
920
983
  S.modelCatalog = models.catalog || [];
921
984
  S.sourceNames = models.source_names || S.sourceNames || {};
985
+ if ($("f-direct-model") && !$('f-direct-model').classList.contains("hidden")) {
986
+ renderDirectModelPicker();
987
+ }
922
988
  // 供应商集合/启停/密钥变了才重拉编排者:否则「生效中」状态点会在停用厂商后失真
923
989
  const provSig = JSON.stringify((S.providers || []).map((p) => [p.id, p.enabled, !!p.api_key]));
924
990
  if (provSig !== S.provSig) { S.provSig = provSig; loadOrchestrator(); }
@@ -2145,6 +2211,7 @@ async function createTask() {
2145
2211
  goal: $("f-goal").value.trim(),
2146
2212
  context: $("f-context").value.trim(),
2147
2213
  workdir: $("f-workdir").value.trim(),
2214
+ thinking: $("f-thinking").value,
2148
2215
  };
2149
2216
  if (!payload.goal) {
2150
2217
  msg.className = "msg err";
@@ -2153,6 +2220,22 @@ async function createTask() {
2153
2220
  resetSubmit();
2154
2221
  return;
2155
2222
  }
2223
+ if (payload.type === "direct" && !S.typeSuggestionDone) {
2224
+ const suggested = recommendTaskType(payload.goal);
2225
+ if (suggested) {
2226
+ const flow = flowById(suggested) || {};
2227
+ resetSubmit();
2228
+ const yes = await uiConfirm(
2229
+ t("这个需求更适合「{0}」流程,能使用对应的规划与质量门禁。是否切换后执行?")
2230
+ .replace("{0}", t(flow.name || suggested)),
2231
+ { title: t("推荐任务类型"), ok: t("切换类型") });
2232
+ S.typeSuggestionDone = true;
2233
+ if (yes) pickType(suggested);
2234
+ $("btn-create").click();
2235
+ return;
2236
+ }
2237
+ }
2238
+ S.typeSuggestionDone = false;
2156
2239
  // 需求拷问闸(借鉴 grill-me):goal 很短且没写背景时先问 1-3 个澄清问题;
2157
2240
  // 采访失败/无问题照常创建——是增强不是闸门
2158
2241
  if (payload.goal.length < 12 && !payload.context && !S.clarifyDone) {
@@ -2198,6 +2281,8 @@ async function createTask() {
2198
2281
  payload.verify_command = $("f-verify").value.trim();
2199
2282
  } else if (_eng === "direct") {
2200
2283
  // 直连:目标+附件即全部输入,不带验证/评审参数
2284
+ payload.direct_provider_id = $("f-direct-provider").value;
2285
+ payload.direct_model = $("f-direct-model-name").value;
2201
2286
  } else {
2202
2287
  payload.manuscript = $("f-manuscript").value.trim() || "manuscript.md";
2203
2288
  payload.rounds = parseInt($("f-rounds").value, 10) || 2;
@@ -2863,11 +2948,19 @@ function newFromTask(id) {
2863
2948
  $("f-workdir").value = tk.workdir || "";
2864
2949
  S.atts = []; renderAttachChips(); // 附件清单不继承:同目录引用已随 context 保留
2865
2950
  queueGitProbe(); // 工作目录变了,重新探测代码版本
2866
- $("f-mode").value = tk.mode === "manual" ? "manual" : "auto";
2951
+ $("f-mode").value = ["fast", "expert", "manual"].includes(tk.mode) ? tk.mode : "auto";
2952
+ $("f-thinking").value = ["low", "standard", "high"].includes(tk.thinking)
2953
+ ? tk.thinking : "auto";
2954
+ $("f-manual-only").classList.toggle("hidden", $("f-mode").value !== "manual");
2867
2955
  renderImplSelects();
2868
2956
  if (tk.implementer) $("f-impl").value = tk.implementer;
2869
2957
  if (flow && flow.engine === "code") {
2870
2958
  $("f-verify").value = tk.verify_command || "";
2959
+ } else if (flow && flow.engine === "direct") {
2960
+ renderDirectModelPicker();
2961
+ $("f-direct-provider").value = tk.direct_provider_id || "";
2962
+ renderDirectModelPicker();
2963
+ $("f-direct-model-name").value = tk.direct_model || "";
2871
2964
  } else {
2872
2965
  $("f-manuscript").value = tk.manuscript || "manuscript.md";
2873
2966
  $("f-rounds").value = tk.rounds || 2;
@@ -3957,7 +4050,8 @@ async function renderRunDetail() {
3957
4050
  '<span class="stat">' + t("创建 ") + '<b>' + esc(run.created_at) + "</b></span>" +
3958
4051
  '<span class="stat">' + t("成本 ") + '<b>$' + Number(run.cost_usd || 0).toFixed(3) + "</b></span>" +
3959
4052
  '<span class="stat">tokens <b>' + (run.tokens || 0) + "</b></span>" +
3960
- (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>" : "") +
3961
4055
  (run.error ? '<span class="stat err">' + errTag(run.error) + esc(run.error.slice(0, 200)) + "</span>" : "") +
3962
4056
  '<span class="stat tasksum hidden" id="rd-meta-task"></span>';
3963
4057
  if (S.detailSide) fillMetaTask(S.detailSide.stats || {}); // 缓存命中:轮询重画不闪丢累计组
@@ -6438,6 +6532,29 @@ function runDurationSeconds(run) {
6438
6532
  return Number.isFinite(a) && Number.isFinite(b) && b >= a ? (b - a) / 1000 : null;
6439
6533
  }
6440
6534
 
6535
+ function runEtaText(run) {
6536
+ if (!run || !Number.isFinite(Number(run.estimated_duration_s))) return "";
6537
+ const estimate = Math.max(1, Number(run.estimated_duration_s));
6538
+ const p90 = Math.max(estimate, Number(run.estimated_p90_s) || estimate);
6539
+ const active = run.status === "running" || run.status === "queued";
6540
+ if (!active) return t("预计完成约 {0}", chatDurTxt(Math.round(estimate)));
6541
+ if (run.status === "queued" || !run.started_at) {
6542
+ return t("预计完成约 {0},保守不超过 {1}",
6543
+ chatDurTxt(Math.round(estimate)), chatDurTxt(Math.round(p90)));
6544
+ }
6545
+ const started = Date.parse(String(run.started_at).replace(" ", "T"));
6546
+ if (!Number.isFinite(started)) return "";
6547
+ const elapsed = Math.max(0, (Date.now() - started) / 1000);
6548
+ if (elapsed < estimate) {
6549
+ return t("预计剩余约 {0}", chatDurTxt(Math.max(1, Math.round(estimate - elapsed))));
6550
+ }
6551
+ if (elapsed < p90) {
6552
+ return t("已超过常规预估,仍在保守区间内(约剩 {0})",
6553
+ chatDurTxt(Math.max(1, Math.round(p90 - elapsed))));
6554
+ }
6555
+ return t("已超过保守预估,正在继续执行,可查看步骤日志");
6556
+ }
6557
+
6441
6558
  function runOutcomeSummary(run) {
6442
6559
  if (!run) return "";
6443
6560
  const last = (run.steps || []).slice().reverse().find((s) => s.output || s.summary);
@@ -6504,6 +6621,7 @@ function renderDetailOverview(run, runs, task) {
6504
6621
  const lastText = last ? String(last.summary || last.output || last.note || "").replace(/\s+/g, " ").trim().slice(0, 240) : "";
6505
6622
  const executor = run.executor || run.implementer || (last && (last.agent_label || last.agent)) || "";
6506
6623
  const duration = runDurationSeconds(run);
6624
+ const etaText = runEtaText(run);
6507
6625
  const goal = (task && task.goal) || run.goal || run.title || "";
6508
6626
  const actions = [];
6509
6627
  if (total) actions.push('<button type="button" class="ghost" data-overview-tab="steps">' + esc(t("查看步骤")) + "</button>");
@@ -6524,6 +6642,7 @@ function renderDetailOverview(run, runs, task) {
6524
6642
  '<span><b>' + settled + "/" + total + '</b> ' + esc(t("步骤已完成")) + '</span>' +
6525
6643
  (executor ? '<span>' + esc(t("执行者")) + ' <b>' + esc(executor) + '</b></span>' : "") +
6526
6644
  (duration != null ? '<span>' + esc(t("耗时")) + ' <b>' + esc(chatDurTxt(duration < 1 ? Number(duration.toFixed(1)) : Math.round(duration))) + '</b></span>' : "") +
6645
+ (etaText ? '<span class="rd-eta">' + esc(etaText) + '</span>' : "") +
6527
6646
  '</div>' +
6528
6647
  (lastText ? '<div class="rd-overview-last"><b>' + esc(t("最近一步")) + '</b><span>' + esc(lastText) + '</span></div>' : "") +
6529
6648
  (actions.length ? '<div class="rd-overview-actions">' + actions.join("") + '</div>' : "");
@@ -9782,7 +9901,7 @@ async function suStartupCheck() {
9782
9901
  }
9783
9902
 
9784
9903
  /* ---------------------------------------------------------- 页签 & 初始化 */
9785
- const TAB_TITLES = { tasks: "任务", runs: "运行记录", automation: "自动化", zentao: "禅道 Bug 自动修复", usage: "用量统计", agents: "本机智能体", models: "模型接入", bindings: "模型调度(可选)", skills: "经验库", knowledge: "知识库", market: "插件市场", orch: "编排设置", data: "数据与备份", appearance: "皮肤", about: "关于与更新" };
9904
+ const TAB_TITLES = { tasks: "任务", runs: "运行记录", automation: "自动化", zentao: "禅道 Bug 自动修复", __wxdigest: "群摘要", usage: "用量统计", agents: "本机智能体", models: "模型接入", bindings: "模型调度(可选)", skills: "经验库", knowledge: "知识库", market: "插件市场", orch: "编排设置", data: "数据与备份", appearance: "皮肤", about: "关于与更新" };
9786
9905
  const SET_TABS = new Set(Object.keys(TAB_TITLES)); // 设置导航里的子页(__phone 是弹框,不算)
9787
9906
 
9788
9907
  function tabTitle(name) {
@@ -9938,7 +10057,7 @@ function setLangBtn(lang) {
9938
10057
  S.catSig = ""; S.modelsSig = ""; S.bindSig = ""; S.orchSig = ""; S.sideSig = ""; S.taskSig = ""; S.mgmt = {};
9939
10058
  syncLangMode();
9940
10059
  // 浏览器标签标题
9941
- document.title = (lang === "en" ? "CodeBee · Multi-agent Orchestrator" : "CodeBee · 多智能体编排台");
10060
+ document.title = codebeeDocumentTitle(lang);
9942
10061
  applyI18n();
9943
10062
  render();
9944
10063
  cmpGreeting(); // 问候语跟随语言重算
@@ -10621,11 +10740,6 @@ function collapseDrawerIfMobile() {
10621
10740
  function switchTab(name) {
10622
10741
  if (name === "__phone") { openPhoneConnect(); return; } // 手机连接是弹框,不切页
10623
10742
  if (name === "__guide") { welcomeOpen(); return; } // 帮助中心是弹层,不切页(设置导航「软件」组)
10624
- if (name === "__wxdigest") { // 群聊汇报沿用全局蜜蜂面板
10625
- beeRefresh().finally(() => beeToggle(true));
10626
- collapseDrawerIfMobile();
10627
- return;
10628
- }
10629
10743
  // 导航收进「设置」:进设置后左栏整体换成设置导航,内容铺满
10630
10744
  S.tab = name;
10631
10745
  if (SET_TABS.has(name)) localStorage.setItem("orch.setTab", name);
@@ -10644,6 +10758,7 @@ function switchTab(name) {
10644
10758
  if (name === "automation") { loadAutomation(); startAutoPoll(); } // 进自动化页:拉取 + 页面可见时每 8s 轮询
10645
10759
  else stopAutoPoll(); // 离开自动化页(或切到别的子页)即停表
10646
10760
  if (name === "zentao") loadZentao(); // 进禅道页:拉配置与修复记录回填表单
10761
+ if (name === "__wxdigest") { beeRefresh(); beeMarkSeen(); } // 进群摘要页:拉取视图并清未读
10647
10762
  if (name === "market") loadMarket(); // 进插件市场页拉取目录
10648
10763
  if (name === "data") loadDataPage(); // 进数据与备份页:清理配置/状态/可清理预估
10649
10764
  if (name === "usage") { syncUsageRange(); loadUsage(); } // 进用量页:对齐范围选中态并拉取
@@ -11111,7 +11226,11 @@ document.addEventListener("DOMContentLoaded", () => {
11111
11226
  });
11112
11227
  $("f-mode").addEventListener("change", () => {
11113
11228
  $("f-manual-only").classList.toggle("hidden", $("f-mode").value !== "manual");
11229
+ refreshEstimate();
11114
11230
  });
11231
+ $("f-thinking").addEventListener("change", refreshEstimate);
11232
+ $("f-rounds").addEventListener("change", refreshEstimate);
11233
+ $("f-direct-provider").addEventListener("change", renderDirectModelPicker);
11115
11234
  $("f-type").dispatchEvent(new Event("change"));
11116
11235
  cmpGreeting(); // 问候语按时段刷新(切语言/回任务页也会重算)
11117
11236
  $("btn-reload-catalog").addEventListener("click", async () => {
@@ -11337,46 +11456,20 @@ function bindCmdK() {
11337
11456
  });
11338
11457
  }
11339
11458
 
11340
- /* ------------------------------------------------ 群摘要蜜蜂坞(core/wxdigest.py)
11341
- * 右下角悬浮蜜蜂:未读摘要徽章+气泡;面板看各群摘要、配置监控文件夹。
11342
- * 桌面蜜蜂 pet.py 的网页侧搭档;入口始终可见,未配置时也能直接打开设置。
11343
- * 60s 轮询 /api/wxdigest;打开面板即 POST seen 清零未读。 */
11344
- const Bee = { view: null, lastUnseen: 0, bubT: 0, selGroups: null };
11459
+ /* ------------------------------------------------ 群摘要页(core/wxdigest.py)
11460
+ * 设置 → 群摘要:配置监控文件夹/微信直连 + 各群摘要列表(替代原右下角悬浮
11461
+ * 蜜蜂坞——用户拍板去掉悬浮组件,配置与总结统一进页面)。桌面蜜蜂 pet.py
11462
+ * 右键「群摘要」直达本页。进页/60s 轮询 /api/wxdigest;进页 POST seen 清未读。 */
11463
+ const Bee = { view: null, lastUnseen: 0, bubT: 0, selGroups: null, pyDirty: false };
11345
11464
 
11346
11465
  function beeShow(el, on) { if (el) el.classList.toggle("hidden", !on); }
11347
- function beePanelOpen() { const p = $("bee-panel"); return !!p && !p.classList.contains("hidden"); }
11348
11466
 
11349
11467
  async function beeRefresh() {
11350
11468
  let v;
11351
11469
  try { v = await api("/api/wxdigest"); } catch (e) { return; } // 静默:不打扰主流程
11352
11470
  Bee.view = v;
11353
- beeShow($("bee-dock"), true);
11354
- beeShow($("bee-badge"), v.unseen > 0);
11355
- if (v.unseen > 0) $("bee-badge").textContent = v.unseen > 99 ? "99+" : String(v.unseen);
11356
- if (v.unseen > Bee.lastUnseen && !beePanelOpen()) { // 新摘要到:气泡 6s
11357
- const b = $("bee-bubble");
11358
- const names = [...new Set((v.digests || []).slice(0, v.unseen).map((d) => d.group))];
11359
- b.textContent = t("嗡!新群摘要好了:") + (names.join(t("、")) || "…");
11360
- beeShow(b, true);
11361
- clearTimeout(Bee.bubT);
11362
- Bee.bubT = setTimeout(() => beeShow(b, false), 6000);
11363
- }
11364
11471
  Bee.lastUnseen = v.unseen;
11365
- if (beePanelOpen()) beeRenderPanel();
11366
- }
11367
-
11368
- function beeToggle(force) {
11369
- const p = $("bee-panel");
11370
- if (!p) return;
11371
- const open = force !== undefined ? force : p.classList.contains("hidden");
11372
- beeShow(p, open);
11373
- if (open) {
11374
- const v = Bee.view || {};
11375
- const hasData = (v.digests || []).length > 0;
11376
- beeShow($("bee-cfg"), !hasData); // 没数据时首开直落配置(可发现性)
11377
- beeRenderPanel();
11378
- beeMarkSeen();
11379
- }
11472
+ beeRenderPanel();
11380
11473
  }
11381
11474
 
11382
11475
  async function beeMarkSeen() {
@@ -11384,7 +11477,6 @@ async function beeMarkSeen() {
11384
11477
  const r = await api("/api/wxdigest/seen", { method: "POST", body: "{}" });
11385
11478
  Bee.view = r.view;
11386
11479
  Bee.lastUnseen = 0;
11387
- beeShow($("bee-badge"), false);
11388
11480
  beeRenderPanel();
11389
11481
  } catch (e) { /* ignore */ }
11390
11482
  }
@@ -11396,7 +11488,10 @@ function beeRenderPanel() {
11396
11488
  $("bee-enabled").checked = !!cfg.enabled;
11397
11489
  $("bee-reader").checked = !!cfg.reader_enabled;
11398
11490
  beeShow($("bee-reader-box"), !!cfg.reader_enabled);
11399
- if (document.activeElement !== $("bee-reader-py")) $("bee-reader-py").value = cfg.reader_python || "";
11491
+ // 用户手动改过路径(选候选/输入)后轮询不再回填覆盖——否则 60s 一冲,
11492
+ // 选择根本存不进去(「选不中 Python」根因之一)
11493
+ if (!Bee.pyDirty && document.activeElement !== $("bee-reader-py"))
11494
+ $("bee-reader-py").value = cfg.reader_python || "";
11400
11495
  if (!Bee.selGroups) Bee.selGroups = (cfg.groups || []).slice();
11401
11496
  const n = Bee.selGroups.length;
11402
11497
  $("bee-groups-btn").textContent = t("选择要监听的群({0})", n);
@@ -11509,6 +11604,7 @@ async function beeSave() {
11509
11604
  }) });
11510
11605
  if (Bee.view) Bee.view.config = r.config;
11511
11606
  Bee.selGroups = null; // 下次渲染用保存后的值重建勾选态
11607
+ Bee.pyDirty = false; // 保存成功:服务端值与所见一致,恢复轮询回填
11512
11608
  toast(t("群摘要设置已保存"));
11513
11609
  beeRefresh();
11514
11610
  } catch (e) { toast(t("保存失败:") + (e.message || e), true); }
@@ -11530,13 +11626,7 @@ async function beeScan() {
11530
11626
 
11531
11627
  (function () {
11532
11628
  const boot = () => {
11533
- if (!$("bee-fab")) return;
11534
- $("bee-fab").addEventListener("click", () => beeToggle());
11535
- $("bee-x").addEventListener("click", () => beeToggle(false));
11536
- $("bee-cfg-btn").addEventListener("click", () => {
11537
- const c = $("bee-cfg");
11538
- beeShow(c, c.classList.contains("hidden"));
11539
- });
11629
+ if (!$("bee-save")) return;
11540
11630
  $("bee-save").addEventListener("click", beeSave);
11541
11631
  $("bee-scan-btn").addEventListener("click", beeScan);
11542
11632
  $("bee-dir-pick").addEventListener("click", () => window.pickFolder("bee-dir", true));
@@ -11552,8 +11642,19 @@ async function beeScan() {
11552
11642
  });
11553
11643
  $("bee-groups-search").addEventListener("input", beeRenderGroups);
11554
11644
  $("bee-groups-refresh").addEventListener("click", beeRefreshGroups);
11645
+ // 64 位 Python 自动扫描 + 一键安装 wechatauto-replica(借鉴用户诉求:
11646
+ // 别手填解释器路径、别自己开 conda 装依赖)
11647
+ $("bee-py-scan").addEventListener("click", beePyScan);
11648
+ $("bee-py-install").addEventListener("click", beePyInstall);
11649
+ // 用户手动改过路径(选候选/输入)后置脏:轮询不再回填覆盖
11650
+ $("bee-reader-py").addEventListener("input", () => { Bee.pyDirty = true; });
11555
11651
  beeRefresh();
11556
11652
  setInterval(beeRefresh, 60000);
11653
+ // 桌宠右键「群摘要」直达:URL 带 #goto=__wxdigest 时切到群摘要子页
11654
+ if (/goto=__wxdigest/.test(location.hash)) {
11655
+ setTimeout(() => switchTab("__wxdigest"), 800);
11656
+ history.replaceState(null, "", location.pathname + location.search);
11657
+ }
11557
11658
  };
11558
11659
  // 错开首屏:令牌门/状态首帧先走,群摘要轮询晚 2s 再起
11559
11660
  if (document.readyState === "loading") {
@@ -11562,3 +11663,93 @@ async function beeScan() {
11562
11663
  setTimeout(boot, 2000);
11563
11664
  }
11564
11665
  })();
11666
+
11667
+ /* ---- 64 位 Python 扫描/一键安装(蜜蜂坞微信直连配置)---- */
11668
+ let beePyTimer = null;
11669
+ async function beePyScan() {
11670
+ const btn = $("bee-py-scan"), box = $("bee-py-cands");
11671
+ if (!btn) return;
11672
+ btn.disabled = true;
11673
+ try { await api("/api/wxdigest/pythons/scan", { method: "POST", body: "{}" }); }
11674
+ catch (e) { toast(t("扫描失败:") + (e.message || e), true); btn.disabled = false; return; }
11675
+ box.classList.remove("hidden");
11676
+ box.innerHTML = '<span class="hint">' + esc(t("正在扫描本机 Python…")) + "</span>";
11677
+ beePyTimer = setInterval(async () => {
11678
+ try {
11679
+ const st = await api("/api/wxdigest/pythons");
11680
+ if (st.scanning) return;
11681
+ clearInterval(beePyTimer); beePyTimer = null;
11682
+ btn.disabled = false;
11683
+ beePyRender(st.results || []);
11684
+ } catch (e) { /* 轮询失败下一拍再试 */ }
11685
+ }, 1500);
11686
+ }
11687
+ function beePyRender(list) {
11688
+ const box = $("bee-py-cands"), st = $("bee-py-status"), ib = $("bee-py-install");
11689
+ if (!box) return;
11690
+ if (!list.length) { box.innerHTML = '<span class="hint">' + esc(t("没有找到可用的 Python,请手动填写路径")) + "</span>"; return; }
11691
+ const cur = ($("bee-reader-py").value || "").trim();
11692
+ box.innerHTML = list.map((p, idx) => {
11693
+ const tag = p.has_replica ? t("已装 wechatauto-replica") : t("未装依赖");
11694
+ const cls = "bee-py-item" + (p.path === cur ? " on" : "");
11695
+ return '<button type="button" class="' + cls + '" data-i="' + idx + '">' +
11696
+ '<span class="bee-py-path">' + esc(p.path) + "</span>" +
11697
+ '<span class="bee-py-meta">' + esc("Py" + p.version + " · " + p.bits + "bit · " + tag) + "</span></button>";
11698
+ }).join("");
11699
+ box.querySelectorAll(".bee-py-item").forEach((b) => {
11700
+ b.addEventListener("click", () => {
11701
+ const p = list[Number(b.dataset.i)];
11702
+ $("bee-reader-py").value = p.path;
11703
+ box.querySelectorAll(".bee-py-item").forEach((x) => x.classList.remove("on"));
11704
+ b.classList.add("on");
11705
+ beePyInstallVisibility(p);
11706
+ });
11707
+ });
11708
+ // 默认选中:当前输入框命中的;否则第一个已装依赖的 64 位
11709
+ const hit = list.find((p) => p.path === cur) ||
11710
+ list.find((p) => p.bits >= 64 && p.has_replica) ||
11711
+ list.find((p) => p.bits >= 64);
11712
+ if (hit) {
11713
+ $("bee-reader-py").value = hit.path;
11714
+ beePyInstallVisibility(hit);
11715
+ const btn = box.querySelector('.bee-py-item[data-i="' + list.indexOf(hit) + '"]');
11716
+ if (btn) btn.classList.add("on");
11717
+ }
11718
+ }
11719
+ function beePyInstallVisibility(p) {
11720
+ const ib = $("bee-py-install"), st = $("bee-py-status");
11721
+ if (!ib) return;
11722
+ const need = p && p.bits >= 64 && !p.has_replica;
11723
+ ib.classList.toggle("hidden", !need);
11724
+ if (st && !need) st.textContent = "";
11725
+ }
11726
+ async function beePyInstall() {
11727
+ const py = ($("bee-reader-py").value || "").trim();
11728
+ if (!py) { toast(t("请先选择或填写 Python 路径"), true); return; }
11729
+ const ib = $("bee-py-install"), st = $("bee-py-status");
11730
+ ib.disabled = true;
11731
+ if (st) st.textContent = t("正在安装 wechatauto-replica…(约 1-3 分钟)");
11732
+ try { await api("/api/wxdigest/replica/install", { method: "POST", body: JSON.stringify({ python: py }) }); }
11733
+ catch (e) {
11734
+ toast(t("安装失败:") + (e.message || e), true);
11735
+ ib.disabled = false;
11736
+ return;
11737
+ }
11738
+ const timer = setInterval(async () => {
11739
+ try {
11740
+ const s = await api("/api/wxdigest/replica/status");
11741
+ if (!s.running) {
11742
+ clearInterval(timer); timer = null;
11743
+ ib.disabled = false;
11744
+ if (s.ok) {
11745
+ if (st) st.textContent = t("安装完成,依赖已就绪");
11746
+ toast(t("wechatauto-replica 安装完成"));
11747
+ } else {
11748
+ if (st) st.textContent = (s.log || []).slice(-3).join(" ").slice(0, 200);
11749
+ toast(t("安装失败,详见状态行"), true);
11750
+ }
11751
+ beePyScan(); // 重扫刷新 has_replica 标记
11752
+ }
11753
+ } catch (e) { /* 轮询失败下一拍再试 */ }
11754
+ }, 3000);
11755
+ }
package/app/ui/i18n.js CHANGED
@@ -28,6 +28,24 @@
28
28
  // —— 表单字段/标签 ——
29
29
 
30
30
  // —— 智能/手动 模式 ——
31
+ "编排模式": "Orchestration mode",
32
+ "自动(推荐):按任务类型与风险动态决定步骤": "Auto (recommended): adapt steps to task type and risk",
33
+ "快速:少步骤优先,先做确定性验收": "Fast: fewer steps with deterministic checks first",
34
+ "专家:完整规划、多评审、失败修复与换将": "Expert: full planning, reviews, repairs, and fallback agents",
35
+ "手动:自行指定实现者与评审组": "Manual: choose implementer and reviewers",
36
+ "思考程度": "Reasoning effort",
37
+ "自动:随任务难度调整": "Auto: adapt to task difficulty",
38
+ "快速:少推理、优先响应": "Fast: less reasoning, faster response",
39
+ "标准:平衡质量与速度": "Standard: balance quality and speed",
40
+ "深度:更多推理与检查": "Deep: more reasoning and checks",
41
+ "对话模型(可选)": "Chat model (optional)",
42
+ "对话厂商": "Chat provider",
43
+ "对话模型": "Chat model",
44
+ "随厂商推荐": "Provider default",
45
+ "只对本次对话生效,不会修改全局绑定;指定渠道不可用时会明确报错。": "Applies only to this chat and does not change global bindings. An unavailable selected route returns an explicit error.",
46
+ "推荐任务类型": "Recommended task type",
47
+ "切换类型": "Switch type",
48
+ "这个需求更适合「{0}」流程,能使用对应的规划与质量门禁。是否切换后执行?": "This request fits the {0} workflow, which provides matching planning and quality gates. Switch before running?",
31
49
 
32
50
  // —— 引擎/角色 ——
33
51
 
@@ -748,6 +766,12 @@
748
766
  "常用类型": "Common types",
749
767
  "预估:≈{0}{1}": "Est: ≈{0}{1}",
750
768
  "近{0}天 · {1}次同类": "last {0}d · {1} similar runs",
769
+ "暂无同类历史,按流程基线估算": "No similar history; estimated from the workflow baseline",
770
+ "预计完成约 {0}": "Estimated completion: about {0}",
771
+ "预计完成约 {0},保守不超过 {1}": "Estimated completion: about {0}; conservative bound {1}",
772
+ "预计剩余约 {0}": "About {0} remaining",
773
+ "已超过常规预估,仍在保守区间内(约剩 {0})": "Past the usual estimate but within the conservative bound (about {0} remaining)",
774
+ "已超过保守预估,正在继续执行,可查看步骤日志": "Past the conservative estimate; still running. Check step logs for progress",
751
775
  "候选并发数(单稿赛马,1=关)": "Best-of-N candidates (1=off)",
752
776
  "同时起草多份候选稿,终审择优并吸收落选稿优点;每多一路多一份起草开销": "Draft several candidates in parallel; a final selector picks the best and harvests the losers' strengths. Each extra candidate costs one more draft",
753
777
  "建 PR": "Create PR",
@@ -1894,6 +1918,18 @@
1894
1918
  "反馈摘要生成失败:": "Failed to build issue summary: ",
1895
1919
  "端口占用": "Port Usage",
1896
1920
  "扫描本机端口": "Scan local ports",
1921
+ "扫描": "Scan",
1922
+ "一键安装 wechatauto-replica": "Install wechatauto-replica",
1923
+ "已装 wechatauto-replica": "replica installed",
1924
+ "未装依赖": "dependency missing",
1925
+ "正在扫描本机 Python…": "Scanning local Pythons…",
1926
+ "没有找到可用的 Python,请手动填写路径": "No usable Python found — please fill in the path manually",
1927
+ "请先选择或填写 Python 路径": "Pick or type a Python path first",
1928
+ "正在安装 wechatauto-replica…(约 1-3 分钟)": "Installing wechatauto-replica… (1-3 minutes)",
1929
+ "安装完成,依赖已就绪": "Installed — dependency ready",
1930
+ "安装失败,详见状态行": "Install failed — see status line",
1931
+ "扫描失败:": "Scan failed: ",
1932
+ "安装失败:": "Install failed: ",
1897
1933
  "谁占着端口、属于哪个项目一目了然;「仅本机」的端口外网不可达。关闭只发温和信号,系统进程与本服务自身拒绝关闭。": "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.",
1898
1934
  "端口": "Port",
1899
1935
  "进程": "Process",