codebee 0.1.28 → 0.1.29

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/CHANGELOG.md CHANGED
@@ -6,6 +6,16 @@ README 元数据带回,供老版本在「发现新版本」时展示新版更
6
6
 
7
7
  ## 未发布
8
8
 
9
+ ## v0.1.29(2026-09-21)
10
+
11
+ ### ✨ 新功能
12
+
13
+ - 升级后蜜蜂自动换新装:修复「升级后还是旧的丑蜜蜂」——服务重启后旧蜜蜂会自动让位,由新服务拉起带新形象的新蜜蜂(此前旧蜜蜂占着位置永不退场)。
14
+
15
+ ### 🛠 问题修复
16
+
17
+ - 去掉蜜蜂底部的任务进度条(真机截图反馈):窗口收紧贴着蜂体,画面更干净。
18
+
9
19
  ## v0.1.28(2026-09-21)
10
20
 
11
21
  ### 🛠 问题修复
package/README.md CHANGED
@@ -21,8 +21,16 @@ Kimi Code、MiMo Code、Grok Build、Pi、DeepSeek Harness……),提供
21
21
  不会把你的任务内容交给任何第三方。
22
22
 
23
23
  <!-- relnotes:start -->
24
+ ### ✨ 新功能 · New
25
+
26
+ - 升级后蜜蜂自动换新装:服务重启后旧蜜蜂自动让位,新蜜蜂带新形象上线。
27
+ - After upgrading, the desktop bee swaps itself for the new look: the old bee steps aside when the service restarts.
28
+
24
29
  ### 🛠 问题修复 · Fixes
25
30
 
31
+ - 去掉蜜蜂底部的任务进度条,窗口更干净。
32
+ - Removed the task progress bar under the desktop bee for a cleaner look.
33
+
26
34
  - 「关于与更新」页的版本信息与更新说明改为左对齐阅读,不再整块居中。
27
35
  - The version info and release notes on the About page are now left-aligned for easier reading.
28
36
 
@@ -103,7 +103,9 @@ def _online_model_bonus(entry, task_type, role):
103
103
  rate = float(metrics.get("success_rate") or 0.0)
104
104
  success_score = max(-6.0, min(6.0, (rate - 0.75) * 18.0))
105
105
  p95 = max(0.0, float(metrics.get("p95_duration_s") or 0.0))
106
- latency_score = -min(4.0, max(0.0, (p95 - 45.0) / 12.0))
106
+ # 模型链也必须让真实延迟影响排序,不能让 P95 数分钟的链靠静态质量分
107
+ # 长期压在十几秒的健康链前面。
108
+ latency_score = -min(12.0, max(0.0, (p95 - 30.0) / 8.0))
107
109
  cost = max(0.0, float(metrics.get("avg_cost_usd") or 0.0))
108
110
  cost_score = -min(3.0, max(0.0, (cost - 0.01) / 0.01))
109
111
  total = round(success_score + latency_score + cost_score, 2)
package/app/core/flows.py CHANGED
@@ -36,7 +36,7 @@ BUILTIN_FLOWS = [
36
36
  "note": "CodeBee 直连模型 API 干活(无 CLI 进程),无可用供应商时回退本机 CLI;无拆解/评审(快)"},
37
37
  {"id": "code", "name": "代码", "icon": "i-code", "engine": "code", "builtin": True,
38
38
  "goal_hint": "要实现/修复什么(一句话)",
39
- "note": "实现 → 验证命令 → 跨厂商评审 → 自动修复/换将"},
39
+ "note": "按难度动态编排:简单任务走实现+验证短链,复杂任务增加规划、评审、修复与换将"},
40
40
  {"id": "novel", "name": "小说", "icon": "i-book-open", "engine": "review", "builtin": True,
41
41
  "manuscript": "manuscript.md",
42
42
  "rubric": ["情节", "人物", "文笔", "节奏", "吸引力"],
package/app/core/jobs.py CHANGED
@@ -309,6 +309,18 @@ def _run_job(job):
309
309
  pass
310
310
  finally:
311
311
  if run_id:
312
+ # 执行线程退出就是生命周期的最后边界。流水线分支若意外提前 return、
313
+ # 或内部吞掉异常却漏写终态,旧逻辑会让 run/task 永久停在 running。
314
+ try:
315
+ from . import store
316
+ latest = store.get_run(run_id)
317
+ if latest and latest.get("status") == "running":
318
+ store.update_run(
319
+ run_id, expected_status="running", status="failed",
320
+ ended_at=_now(),
321
+ error="执行流程已退出但未写入完成状态,已自动标记失败;可重试")
322
+ except Exception:
323
+ pass
312
324
  CANCELS.pop(run_id, None)
313
325
  try:
314
326
  _maybe_auto_resume(run_id)
@@ -944,14 +944,20 @@ def _run_code(run, task, agents, ev, stats, mode):
944
944
  error="没有可用智能体", ended_at=_now())
945
945
  return
946
946
 
947
- # ---- 规划
948
- if mode == "auto":
947
+ # 项目记忆既给规划器,也给快速路径的实现者;不能因省掉独立规划而漏掉。
948
+ user_context_len = len(task.get("context") or "")
949
+ project_memory = _read_project_memory(workdir)
950
+ if project_memory:
951
+ task = dict(task, context=(task.get("context") or "") + "\n\n" + project_memory)
952
+
953
+ # ---- 规划。低风险短任务直接把目标作为单步计划,省掉一次独立模型调用;
954
+ # 附件或较长的用户背景仍先规划,避免快速路径漏读约束。
955
+ fast_path = (mode == "auto" and difficulty == "easy"
956
+ and not task.get("attachments")
957
+ and user_context_len < 600)
958
+ planned = False
959
+ if mode == "auto" and not fast_path:
949
960
  _wait_gate(run_id, ev)
950
- # 项目记忆注入(借鉴 agentmemory 持久记忆):同工作目录此前代码任务留下的
951
- # 架构事实,让规划器不再对代码库一无所知
952
- _pm = _read_project_memory(workdir)
953
- if _pm:
954
- task = dict(task, context=(task.get("context") or "") + "\n\n" + _pm)
955
961
  plan_step, plan_log = store.add_step(run_id, "plan", impl["id"], impl.get("label"),
956
962
  note=route.get("implementer", ""))
957
963
  plan = planner.make_code_plan(_steered_task(run_id, task),
@@ -967,21 +973,39 @@ def _run_code(run, task, agents, ev, stats, mode):
967
973
  plan["source"], difficulty,
968
974
  ";".join(s["title"] for s in plan["steps"])[:160]),
969
975
  duration_s=0.1 if impl.get("mode") == "mock" else None)
976
+ planned = True
977
+ elif fast_path:
978
+ plan = {
979
+ "source": "dynamic-fast-path",
980
+ "difficulty": "easy",
981
+ "steps": [{"title": "直接实现并自检", "detail": task["goal"]}],
982
+ "workflow": {
983
+ "review_required": not bool(task.get("verify_command")),
984
+ "max_repair_rounds": 1,
985
+ "allow_switch": False,
986
+ },
987
+ }
970
988
  else:
971
989
  plan = {"source": "manual", "steps": [{"title": "实现任务", "detail": task["goal"]}]}
972
- store.update_run(run_id, plan=plan, difficulty=difficulty)
990
+ workflow = task_compile.code_workflow(
991
+ task, difficulty, plan=plan, mode=mode, planned=planned)
992
+ store.update_run(run_id, plan=plan, difficulty=difficulty, workflow=workflow)
973
993
  subtasks = plan["steps"]
974
994
  # 计划落盘(planning-with-files):磁盘上的计划与 spec/evidence 同居任务档案
975
995
  _write_task_plan(task, workdir, plan)
976
996
 
977
997
  # ---- 评审者(有会话延续时评审者仍用新鲜上下文,避免偏见)
978
- if mode == "auto":
998
+ if not workflow["review_required"]:
999
+ reviewer = None
1000
+ route["reviewer"] = workflow["reason"]
1001
+ elif mode == "auto":
979
1002
  reviewer, route["reviewer"] = router.pick_reviewer(agents, impl, "code", stats)
980
1003
  else:
981
1004
  reviewer, route["reviewer"] = _pick_reviewer_legacy(agents, impl)
982
1005
  _record_actual_route(run_id, task, agents, stats, impl, reviewer=reviewer,
983
1006
  implement_reason=route.get("implementer", ""),
984
- review_reason=route.get("reviewer", ""))
1007
+ review_reason=route.get("reviewer", ""),
1008
+ direct=not workflow["review_required"])
985
1009
  store.update_run(run_id, route=route)
986
1010
 
987
1011
  def implement_all(impl_agent, prefix_note):
@@ -1078,7 +1102,8 @@ def _run_code(run, task, agents, ev, stats, mode):
1078
1102
  run_id, task, agents, stats, other,
1079
1103
  implementers=[other], reviewer=reviewer,
1080
1104
  implement_reason=note,
1081
- review_reason=route.get("reviewer", ""))
1105
+ review_reason=route.get("reviewer", ""),
1106
+ direct=not workflow["review_required"])
1082
1107
  return True
1083
1108
  res_err = "%s;换将后仍失败:%s" % (note, (res.get("error") or "")[:200])
1084
1109
  else:
@@ -1091,6 +1116,17 @@ def _run_code(run, task, agents, ev, stats, mode):
1091
1116
 
1092
1117
  def review_and_score():
1093
1118
  verify_pass, verify_ran = _run_verify(run_id, task, workdir, ev)
1119
+ if not workflow["review_required"]:
1120
+ return {
1121
+ "pass": verify_pass,
1122
+ "scores": {},
1123
+ "issues": ([] if verify_pass else [{
1124
+ "severity": "high", "title": "验证命令未通过",
1125
+ "detail": "动态短链省略模型评审,先修复确定性验证失败。",
1126
+ }]),
1127
+ "summary": "低风险短链:确定性验证%s,模型评审已省略。" %
1128
+ ("通过" if verify_pass else "未通过"),
1129
+ }, verify_pass, verify_ran
1094
1130
  # gate 验证(借鉴 pi-subagents 的 gate:"npm test"):verify 失败时先跳过
1095
1131
  # 模型评审直接进修复轮——评审此时只能复述「验证没过」,白烧一次调用。
1096
1132
  # 连续失败 >=2 轮后恢复评审参与诊断(模型能看出编译错误之外的病灶)。
@@ -1138,7 +1174,8 @@ def _run_code(run, task, agents, ev, stats, mode):
1138
1174
  implementers=[impl], reviewer=reviewer,
1139
1175
  implement_reason="修复轮由 %s 完成" %
1140
1176
  (impl.get("label") or impl.get("id")),
1141
- review_reason=route.get("reviewer", ""))
1177
+ review_reason=route.get("reviewer", ""),
1178
+ direct=not workflow["review_required"])
1142
1179
  if impl.get("mode") == "mock" and res["ok"]:
1143
1180
  pass # mock 不产生真实变更
1144
1181
  review_json, verify_pass, verify_ran = review_and_score()
@@ -1149,10 +1186,10 @@ def _run_code(run, task, agents, ev, stats, mode):
1149
1186
  "passed": passed})
1150
1187
  if passed:
1151
1188
  break
1152
- if round_no < router.MAX_REPAIR_ROUNDS and not switched:
1189
+ if round_no < workflow["max_repair_rounds"] and not switched:
1153
1190
  round_no += 1
1154
1191
  continue
1155
- if mode == "auto" and not switched:
1192
+ if mode == "auto" and workflow["allow_switch"] and not switched:
1156
1193
  ex = (impl["id"],)
1157
1194
  if impl.get("mode") == "real":
1158
1195
  ex += ("mock-a", "mock-b") # 真实实现者失败时不降级到 mock
@@ -1166,7 +1203,8 @@ def _run_code(run, task, agents, ev, stats, mode):
1166
1203
  run_id, task, agents, stats, impl,
1167
1204
  implementers=[impl], reviewer=reviewer,
1168
1205
  implement_reason=note,
1169
- review_reason=route.get("reviewer", ""))
1206
+ review_reason=route.get("reviewer", ""),
1207
+ direct=not workflow["review_required"])
1170
1208
  switched = True
1171
1209
  round_no += 1
1172
1210
  continue
@@ -1178,18 +1216,27 @@ def _run_code(run, task, agents, ev, stats, mode):
1178
1216
  verdict = {
1179
1217
  "type": "code", "engine": "code", "pass": overall_pass, "mode": mode,
1180
1218
  "verify_ran": verify_ran, "verify_pass": verify_pass,
1181
- "review_pass": bool(review_json.get("pass")),
1219
+ "review_pass": (bool(review_json.get("pass"))
1220
+ if workflow["review_required"] else None),
1221
+ "review_skipped": not workflow["review_required"],
1182
1222
  "scores": scores, "overall_score": overall_score,
1183
1223
  "issues": review_json.get("issues") or [],
1184
- "reviewer": reviewer["id"],
1224
+ "reviewer": reviewer["id"] if reviewer else "",
1185
1225
  "route": route, "repairs": repairs, "switched": switched,
1226
+ "workflow": workflow,
1186
1227
  }
1187
1228
 
1188
1229
  lines = [
1189
1230
  "# 代码任务报告:%s" % task["title"], "",
1190
1231
  "- 结论:**%s**" % ("✅ 通过" if overall_pass else "❌ 未通过"),
1191
1232
  "- 编排模式:%s 实现者:%s 评审者:%s" % (
1192
- "智能" if mode == "auto" else "手动", impl.get("label"), reviewer.get("label")),
1233
+ "智能" if mode == "auto" else "手动", impl.get("label"),
1234
+ reviewer.get("label") if reviewer else "按验证结果省略"),
1235
+ "- 动态步骤:规划 %s;实现 %d 步;验证 %s;评审 %d 人;最多修复 %d 轮;换将 %s" % (
1236
+ "独立执行" if workflow["planning"] == "llm" else "并入实现",
1237
+ workflow["implementation_steps"], "有" if workflow["verification"] else "无",
1238
+ workflow["reviewers"], workflow["max_repair_rounds"],
1239
+ "允许" if workflow["allow_switch"] else "关闭"),
1193
1240
  "- 验证命令:%s → %s" % (task.get("verify_command") or "(未配置)",
1194
1241
  "通过" if verify_pass else "未通过"),
1195
1242
  "- 综合评分:%s 修复/换将:%s" % (
@@ -1229,7 +1276,8 @@ def _run_code(run, task, agents, ev, stats, mode):
1229
1276
  summary="代码任务%s(验证%s / 评审%s%s)" % (
1230
1277
  "通过" if overall_pass else "未通过",
1231
1278
  "通过" if verify_pass else "未通过",
1232
- "通过" if review_json.get("pass") else "未通过",
1279
+ (("通过" if review_json.get("pass") else "未通过")
1280
+ if workflow["review_required"] else "按策略省略"),
1233
1281
  ",%d 轮修复" % (len(repairs) - 1) if len(repairs) > 1 else ""),
1234
1282
  ended_at=_now())
1235
1283
 
@@ -114,11 +114,13 @@ def _log_usage(source, role, task, res, agent=None, tool="", model="", provider=
114
114
  except Exception:
115
115
  pass
116
116
 
117
- CODE_PLAN_PROMPT = """你是技术负责人。请把下面的开发目标拆解为 __N__ 个以内、按顺序执行的子任务,
118
- 并判定任务难度。只输出一个 ```json 代码块,不要输出其他内容。JSON 结构:
119
- {"difficulty": "easy 或 hard", "subtasks": [{"title": "简短标题", "detail": "具体要做什么,给执行工程师的直接指令", "files": ["涉及的文件路径"]}]}
120
- 难度判定:常规增删改查/小函数/格式调整 = easy;跨模块改动/架构调整/复杂算法/安全相关 = hard。
121
- 子任务粒度要可独立验证;最后一个子任务必须包含整体联调/收尾。
117
+ CODE_PLAN_PROMPT = """你是技术负责人。请把下面的开发目标拆解为 __N__ 个以内、按顺序执行的子任务,
118
+ 并判定任务难度和后续质量步骤。只输出一个 ```json 代码块,不要输出其他内容。JSON 结构:
119
+ {"difficulty": "easy 或 hard", "workflow": {"review_required": true, "max_repair_rounds": 0, "allow_switch": false}, "subtasks": [{"title": "简短标题", "detail": "具体要做什么,给执行工程师的直接指令", "files": ["涉及的文件路径"]}]}
120
+ 难度判定:常规增删改查/小函数/格式调整 = easy;跨模块改动/架构调整/复杂算法/安全相关 = hard。
121
+ 质量步骤判定:涉及安全、数据迁移、并发、权限、公共 API 或跨模块改动时必须评审;
122
+ 有明确自动验收的低风险小改可不做模型评审。max_repair_rounds 取 0-2,只有复杂任务才建议换将。
123
+ 子任务粒度要可独立验证;最后一个子任务必须包含整体联调/收尾。
122
124
 
123
125
  ## 先探索再计划(重要,借鉴 OpenSpec explore)
124
126
  拆解之前先用你的读文件/搜索工具**实际查看工作目录**,找到目标相关的真实文件与函数,再据此拆解。
@@ -135,9 +137,12 @@ __CONTEXT__
135
137
  ## 验收命令(最终必须通过)
136
138
  __VERIFY__"""
137
139
 
138
- REVIEW_OUTLINE_PROMPT = """你是内容主编。请为下面的创作任务拟一份写作大纲(要点列表,3-8 条),
139
- 只输出一个 ```json 代码块,不要输出其他内容。JSON 结构:
140
- {"outline": ["要点1", "要点2", ...]}
140
+ REVIEW_OUTLINE_PROMPT = """你是内容主编。请为下面的创作任务拟一份写作大纲(要点列表,3-8 条),
141
+ 并按内容风险建议后续质量步骤。
142
+ 只输出一个 ```json 代码块,不要输出其他内容。JSON 结构:
143
+ {"outline": ["要点1", "要点2", ...], "workflow": {"reviewers": 1, "review_rounds": 1}}
144
+ reviewers 取 1-2,review_rounds 取 1-3;研究报告、技术方案、长文和高发布门槛应增加,
145
+ 普通邮件、短翻译、工作汇报等低风险短内容保持 1。
141
146
 
142
147
  ## 创作任务
143
148
  __GOAL__
@@ -403,9 +408,27 @@ def _norm_subtasks(data):
403
408
  return steps or None
404
409
 
405
410
 
406
- def _plan_difficulty(data):
411
+ def _plan_difficulty(data):
407
412
  d = str((data or {}).get("difficulty") or "").lower()
408
- return d if d in ("easy", "hard") else None
413
+ return d if d in ("easy", "hard") else None
414
+
415
+
416
+ def _plan_workflow(data):
417
+ """规范化规划器给出的后续步骤建议;最终安全下限由任务编译器裁决。"""
418
+ raw = (data or {}).get("workflow")
419
+ if not isinstance(raw, dict):
420
+ return None
421
+ out = {}
422
+ if isinstance(raw.get("review_required"), bool):
423
+ out["review_required"] = raw["review_required"]
424
+ try:
425
+ if "max_repair_rounds" in raw:
426
+ out["max_repair_rounds"] = max(0, min(2, int(raw["max_repair_rounds"])))
427
+ except (TypeError, ValueError):
428
+ pass
429
+ if isinstance(raw.get("allow_switch"), bool):
430
+ out["allow_switch"] = raw["allow_switch"]
431
+ return out or None
409
432
 
410
433
 
411
434
  def _orchestrator():
@@ -437,9 +460,10 @@ def make_code_plan(task, planner_agent, workdir, ev=None, resume=None, log_path=
437
460
  _log_usage("plan", "plan", task, res, agent=planner_agent)
438
461
  data = runner.extract_json(res.get("text") or "")
439
462
  steps = _norm_subtasks(data)
440
- if steps:
441
- return {"source": "llm(%s)" % planner_agent["id"], "steps": steps,
442
- "difficulty": _plan_difficulty(data)}
463
+ if steps:
464
+ return {"source": "llm(%s)" % planner_agent["id"], "steps": steps,
465
+ "difficulty": _plan_difficulty(data),
466
+ "workflow": _plan_workflow(data)}
443
467
  return _fallback_code_plan(task, "LLM 计划解析失败,退化为单步模板")
444
468
 
445
469
 
@@ -468,8 +492,9 @@ def _orch_code_plan(task, prov, model, log_path=None):
468
492
  if not steps:
469
493
  _append_log(log_path, "编排者计划返回内容无法解析为子任务,回退 CLI")
470
494
  return None
471
- return {"source": "编排者(%s · %s)" % (prov.get("name", prov["id"]), model),
472
- "steps": steps, "difficulty": _plan_difficulty(data)}
495
+ return {"source": "编排者(%s · %s)" % (prov.get("name", prov["id"]), model),
496
+ "steps": steps, "difficulty": _plan_difficulty(data),
497
+ "workflow": _plan_workflow(data)}
473
498
 
474
499
 
475
500
  def make_review_outline(task):
@@ -492,8 +517,20 @@ def make_review_outline(task):
492
517
  if not isinstance(outline, list):
493
518
  return None
494
519
  items = [str(x).strip()[:120] for x in outline if str(x).strip()][:8]
495
- return {"source": "编排者(%s · %s)" % (prov.get("name", prov["id"]), model),
496
- "items": items} if items else None
520
+ if not items:
521
+ return None
522
+ workflow = data.get("workflow") if isinstance(data.get("workflow"), dict) else {}
523
+ try:
524
+ reviewers = max(1, min(2, int(workflow.get("reviewers") or 1)))
525
+ except (TypeError, ValueError):
526
+ reviewers = 1
527
+ try:
528
+ review_rounds = max(1, min(3, int(workflow.get("review_rounds") or 1)))
529
+ except (TypeError, ValueError):
530
+ review_rounds = 1
531
+ return {"source": "编排者(%s · %s)" % (prov.get("name", prov["id"]), model),
532
+ "items": items,
533
+ "workflow": {"reviewers": reviewers, "review_rounds": review_rounds}}
497
534
 
498
535
 
499
536
  def make_novel_plan(task, author, critics):
@@ -11,6 +11,7 @@ CAPABILITY = {
11
11
  }
12
12
 
13
13
  MAX_REPAIR_ROUNDS = 2 # 自动修复循环上限
14
+ AUTO_CRITIC_LIMIT = 2 # 自动内容评审只取综合分前两名;手工模式仍尊重完整名单
14
15
 
15
16
 
16
17
  def _task_type(ttype):
@@ -62,7 +63,9 @@ def _online_bonus(agent, role, ttype):
62
63
  rate = float(metrics.get("success_rate") or 0.0)
63
64
  success_score = max(-8.0, min(8.0, (rate - 0.75) * 24.0))
64
65
  p95 = max(0.0, float(metrics.get("p95_duration_s") or 0.0))
65
- latency_score = -min(6.0, max(0.0, (p95 - 45.0) / 10.0))
66
+ # 旧上限 -6 抵不过 CLI 静态能力 10 分差,P95 两三分钟的模型仍会
67
+ # 压过十几秒的健康模型。加大惩罚,让真实延迟足以改变自动选择。
68
+ latency_score = -min(18.0, max(0.0, (p95 - 30.0) / 7.0))
66
69
  cost = max(0.0, float(metrics.get("avg_cost_usd") or 0.0))
67
70
  cost_score = -min(4.0, max(0.0, (cost - 0.01) / 0.01))
68
71
  total = round(success_score + latency_score + cost_score, 1)
@@ -199,14 +202,18 @@ def pick_reviewer(agents, impl, ttype, stats=None):
199
202
  return impl, "(自评:仅有实现者一个智能体可用)"
200
203
 
201
204
 
202
- def pick_critics(agents, ttype, stats=None, impl=None):
203
- """小说评审组:全部真实智能体(排除作者本人——自评是最典型的同族盲区);
204
- 面板与作者同厂商时在路由依据里明示,供用户决定是否补充其他厂商。
205
- 没有任何真实智能体时用内置 mock。"""
206
- stats = stats or history.agent_stats()
207
- real = [a for a in agents if a.get("mode") == "real" and (not impl or a["id"] != impl["id"])]
208
- if real:
209
- note = "全部真实智能体参与(按历史表现自动选择)"
205
+ def pick_critics(agents, ttype, stats=None, impl=None):
206
+ """自动内容评审组选综合分前两名(排除作者本人)。
207
+
208
+ 旧逻辑让全部已启用 CLI 串行参与每章和全局评审;一条慢/失效链就会把
209
+ 任务拖长数分钟。两名保留交叉判断并封顶默认调用数;手工模式不走这里。
210
+ """
211
+ stats = stats or history.agent_stats()
212
+ real = [a for a in agents if a.get("mode") == "real" and (not impl or a["id"] != impl["id"])]
213
+ if real:
214
+ ranked = sorted(real, key=lambda a: score(a, "review", ttype, stats)[0], reverse=True)
215
+ real = ranked[:AUTO_CRITIC_LIMIT]
216
+ note = "按成功率与延迟选择前 %d 名评审(共 %d 名候选)" % (len(real), len(ranked))
210
217
  if impl and impl.get("mode") == "real" \
211
218
  and all(a.get("kind") == impl.get("kind") for a in real):
212
219
  note += ";评审组与作者同为 %s,建议启用其他厂商 CLI 交叉评审" % impl.get("kind")
@@ -188,16 +188,21 @@ def apply_upgrade(port=None):
188
188
  return {"error": "升级任务创建失败,请稍后重试"}
189
189
  try:
190
190
  jobs.enqueue({"kind": "selfupgrade", "run_id": run["id"]})
191
- except Exception:
192
- # run 已持久化;启动失败时显式收口,版本页不能停在误导性的待启动状态。
193
- log.exception("selfupdate: 升级任务启动失败 run=%s", run["id"])
194
- try:
195
- store.update_run(run["id"], status="failed",
196
- error="升级任务启动失败,本次未排队,请稍后重试",
197
- ended_at=time.strftime("%Y-%m-%d %H:%M:%S"))
191
+ except Exception as exc:
192
+ # run 已持久化;启动失败时显式收口,版本页不能停在误导性的待启动状态。
193
+ log.exception("selfupdate: 升级任务启动失败 run=%s", run["id"])
194
+ if isinstance(exc, jobs.JobsBusyError):
195
+ message = "升级未启动:%s" % str(exc)
196
+ else:
197
+ # 仅并发保护类错误可直接展示;其他内部异常继续隐藏实现细节。
198
+ message = "升级任务启动失败,本次未排队,请稍后重试"
199
+ try:
200
+ store.update_run(run["id"], status="failed",
201
+ error=message,
202
+ ended_at=time.strftime("%Y-%m-%d %H:%M:%S"))
198
203
  except Exception:
199
204
  log.exception("selfupdate: 升级运行失败收口失败 run=%s", run["id"])
200
- return {"error": "升级任务启动失败,本次未排队,请稍后重试", "run_id": run["id"]}
205
+ return {"error": message, "run_id": run["id"]}
201
206
  return {"run_id": run["id"]}
202
207
 
203
208
 
@@ -85,3 +85,93 @@ def summary(spec):
85
85
  spec.get("type") or "direct", spec.get("engine") or "direct",
86
86
  spec.get("difficulty") or "default",
87
87
  "、".join(spec.get("capabilities") or []) or "通用")
88
+
89
+
90
+ def code_workflow(task, difficulty, plan=None, mode="auto", planned=True):
91
+ """把难度、验收能力和规划建议收敛成代码任务的实际步骤策略。
92
+
93
+ 自动模式允许低风险任务走短链;手动模式与高风险任务保留完整质量门禁。
94
+ 规划器只能在安全边界内减少步骤,不能让无确定性验证的任务跳过评审。
95
+ """
96
+ task = task if isinstance(task, Mapping) else {}
97
+ rec = (plan or {}).get("workflow") if isinstance(plan, Mapping) else None
98
+ rec = rec if isinstance(rec, Mapping) else {}
99
+ has_verify = bool(str(task.get("verify_command") or "").strip())
100
+ easy_auto = mode == "auto" and difficulty == "easy"
101
+
102
+ review_required = True
103
+ max_repairs = 2
104
+ allow_switch = True
105
+ reason = "复杂或手动任务使用完整质量门禁"
106
+ if easy_auto:
107
+ review_required = not has_verify
108
+ max_repairs = 1
109
+ allow_switch = False
110
+ reason = ("低风险任务以确定性验证代替模型评审"
111
+ if has_verify else "低风险任务无验证命令,保留一次模型评审")
112
+ if isinstance(rec.get("review_required"), bool):
113
+ review_required = rec["review_required"] or not has_verify
114
+ try:
115
+ if "max_repair_rounds" in rec:
116
+ max_repairs = max(0, min(1, int(rec["max_repair_rounds"])))
117
+ except (TypeError, ValueError):
118
+ pass
119
+ elif mode == "auto" and difficulty == "hard":
120
+ try:
121
+ if "max_repair_rounds" in rec:
122
+ max_repairs = max(1, min(2, int(rec["max_repair_rounds"])))
123
+ except (TypeError, ValueError):
124
+ pass
125
+ if isinstance(rec.get("allow_switch"), bool):
126
+ allow_switch = rec["allow_switch"]
127
+ reason = "复杂任务保留规划、验证与模型评审"
128
+
129
+ return {
130
+ "planning": "llm" if planned else "inline",
131
+ "implementation_steps": len((plan or {}).get("steps") or []) or 1,
132
+ "verification": has_verify,
133
+ "review_required": review_required,
134
+ "reviewers": 1 if review_required else 0,
135
+ "max_repair_rounds": max_repairs,
136
+ "allow_switch": allow_switch,
137
+ "reason": reason,
138
+ }
139
+
140
+
141
+ def content_workflow(task, difficulty, plan=None, mode="auto"):
142
+ """为非连载内容任务生成动态大纲/评审/修订策略。"""
143
+ task = task if isinstance(task, Mapping) else {}
144
+ ttype = str(task.get("type") or "doc")
145
+ requested_rounds = max(1, min(5, int(task.get("rounds") or 2)))
146
+ rec = (plan or {}).get("workflow") if isinstance(plan, Mapping) else None
147
+ rec = rec if isinstance(rec, Mapping) else {}
148
+ light_types = {"email", "weekly_report", "translation"}
149
+ deep_types = {"novel", "research", "tech_proposal"}
150
+
151
+ if mode != "auto":
152
+ return {"outline": True, "reviewers": 0,
153
+ "review_rounds": requested_rounds,
154
+ "reason": "手动模式尊重用户指定的评审组与轮数"}
155
+
156
+ if ttype in light_types and difficulty != "hard":
157
+ return {"outline": False, "reviewers": 1, "review_rounds": 1,
158
+ "reason": "轻量内容省略独立大纲,只做一次快速评审"}
159
+
160
+ outline = ttype in deep_types or difficulty == "hard"
161
+ reviewers = 2 if outline else 1
162
+ review_rounds = min(requested_rounds, 2 if outline else 1)
163
+ try:
164
+ if "reviewers" in rec:
165
+ reviewers = max(1, min(2, int(rec["reviewers"])))
166
+ if "review_rounds" in rec:
167
+ review_rounds = max(1, min(requested_rounds, 3,
168
+ int(rec["review_rounds"])))
169
+ except (TypeError, ValueError):
170
+ pass
171
+ # 深度类型至少保留一轮评审,但允许规划器把双评审降成一名;高门槛强制双评审。
172
+ if float(task.get("threshold") or 7.0) >= 8.5:
173
+ reviewers = 2
174
+ return {"outline": outline, "reviewers": reviewers,
175
+ "review_rounds": review_rounds,
176
+ "reason": ("深度内容保留大纲和多轮质量门禁" if outline
177
+ else "标准内容使用单评审短链")}
package/app/main.py CHANGED
@@ -52,6 +52,7 @@ def _utf8_bytes(data):
52
52
  return data.decode("utf-8", "replace").encode("utf-8")
53
53
 
54
54
  PORT = 8765 # main() 启动时更新;/api/connect 组装扫码地址用
55
+ _BOOT_TS = time.time() # 服务进程启动时间戳:/api/pet_state 下发,蜜蜂据此识别「服务换人了」并让位
55
56
 
56
57
  # 写接口统一限制 JSON 请求体,避免误传文件或异常客户端把 worker 线程和
57
58
  # 内存拖垮。16 MiB 足够覆盖任务上下文、故事圣经和附件清单(附件本体走
@@ -1468,6 +1469,7 @@ class Handler(BaseHTTPRequestHandler):
1468
1469
  「刚完工/刚出事」。设置随响应回传,蜜蜂轮询到 pet_enabled=false 自行
1469
1470
  退出——关闭桌宠不需要专门的信号通道。
1470
1471
  """
1472
+ global _BOOT_TS
1471
1473
  st = settings.load()
1472
1474
  rows = []
1473
1475
  n_run = n_q = 0
@@ -1500,6 +1502,10 @@ class Handler(BaseHTTPRequestHandler):
1500
1502
  dg = {"unseen": 0, "latest": None}
1501
1503
  return self._json(200, {
1502
1504
  "ok": True, "ts": int(time.time()), "port": PORT,
1505
+ # boot:本服务进程的启动时间戳。蜜蜂记住首次见到的值,值变了说明
1506
+ # 服务换了个进程(升级重启)——旧蜜蜂自行退场让新服务 spawn 新蜜蜂
1507
+ # (旧形象常驻的根因:同端口轮询永远正常,旧进程从不自离)。
1508
+ "boot": _BOOT_TS,
1503
1509
  "settings": {"pet_enabled": bool(st.get("pet_enabled", True)),
1504
1510
  "pet_mode": str(st.get("pet_mode") or "always"),
1505
1511
  "pet_skin": str(st.get("pet_skin") or "plush")},
package/app/pet.py CHANGED
@@ -82,6 +82,8 @@ def parse_snapshot(raw):
82
82
  "error": str(t.get("error") or ""),
83
83
  })
84
84
  return {
85
+ # boot:服务进程启动标识(值变化=服务换人,本宠让位给新服务的蜜蜂)
86
+ "boot": raw.get("boot") or None,
85
87
  "settings": {
86
88
  "pet_enabled": bool(st.get("pet_enabled", True)),
87
89
  "pet_mode": str(st.get("pet_mode") or "always"),
@@ -309,7 +311,6 @@ KEY_RGB = (1, 2, 3)
309
311
  WIN_W, WIN_H = 170, 150 # 手绘回落模式的窗口尺寸
310
312
  SPRITE_DIR = Path(__file__).resolve().parent
311
313
  SPRITE_DISP_H = 112 # 精灵显示高度(宽等比)
312
- PROG_H = 16 # 底部进度条预留高度(有进度时才显示)
313
314
 
314
315
 
315
316
  def _pid_alive(pid):
@@ -371,6 +372,9 @@ _BROWSER_EXES = {
371
372
  }
372
373
 
373
374
 
375
+ petWindow = None
376
+
377
+
374
378
  def _window_process_name(hwnd):
375
379
  """取顶层窗口所属进程的可执行文件名(小写);失败返回空串。"""
376
380
  try:
@@ -438,6 +442,38 @@ def _focus_codebee_window(ignore_hwnd=None):
438
442
  return False
439
443
 
440
444
 
445
+ def _show_pet_window(ignore_hwnd=None):
446
+ """复用桌宠拉起的 CodeBee 浏览器窗口;窗口已关闭时返回 False。"""
447
+ global petWindow
448
+ try:
449
+ if os.name != "nt":
450
+ return False
451
+ import ctypes
452
+ user32 = ctypes.windll.user32
453
+ if petWindow and user32.IsWindow(petWindow):
454
+ if user32.IsIconic(petWindow):
455
+ user32.ShowWindow(petWindow, 9) # SW_RESTORE
456
+ user32.SetForegroundWindow(petWindow)
457
+ return True
458
+ petWindow = None
459
+ hwnd = _find_codebee_window(ignore_hwnd)
460
+ if not hwnd:
461
+ return False
462
+ petWindow = hwnd
463
+ if user32.IsIconic(hwnd):
464
+ user32.ShowWindow(hwnd, 9)
465
+ user32.SetForegroundWindow(hwnd)
466
+ return True
467
+ except Exception:
468
+ return False
469
+
470
+
471
+ def openPetWindow(port, ignore_hwnd=None):
472
+ if os.name == "nt" and _show_pet_window(ignore_hwnd):
473
+ return
474
+ webbrowser.open("http://127.0.0.1:%d" % port)
475
+
476
+
441
477
  class PetApp:
442
478
  """蜜蜂窗口:蜜蜂本体(精灵/手绘)+ 状态机动画 + 悬停任务清单 + 右键菜单。"""
443
479
 
@@ -485,6 +521,7 @@ class PetApp:
485
521
  # 运行态
486
522
  self.snap = None
487
523
  self.miss = 0
524
+ self.boot = None # 首次见到的服务 boot 标识;变化=服务换进程,本宠让位
488
525
  self.prev_active = set()
489
526
  self.mood_kind, self.mood_until = "", 0.0
490
527
  self.last_busy = time.time()
@@ -638,18 +675,19 @@ class PetApp:
638
675
  def _size_for(self, frames):
639
676
  fw = max(f.width() for lst in frames.values() for f in lst)
640
677
  fh = max(f.height() for lst in frames.values() for f in lst)
641
- return int(fw) + 8, int(fh) + PROG_H + 4
678
+ return int(fw) + 8, int(fh) + 4
642
679
 
643
680
  # ---- 蜜蜂绘制(精灵模式:贴图 + 状态点缀画件)----
644
- # 用户拍板去掉:右上徽章、左侧速度线、底部影子(真机截图圈删)——
645
- # 窗口贴着蜂体开,点缀只留睡觉 Zzz / 庆祝星光 / 底部进度条。
681
+ # 用户拍板去掉:右上徽章、左侧速度线、底部影子(真机截图圈删)、
682
+ # 底部任务进度条(2026-09-21 真机截图圈删)——
683
+ # 窗口贴着蜂体开,点缀只留睡觉 Zzz / 庆祝星光。
646
684
  def _build_sprite(self):
647
685
  cv = self.cv
648
- cx, cy = self.win_w // 2, (self.win_h - PROG_H) // 2 + 2
686
+ cx, cy = self.win_w // 2, self.win_h // 2 + 2
649
687
  self._spr_c = (cx, cy)
650
688
  self.spr = cv.create_image(cx, cy, image=self.frames["alert"][0],
651
689
  anchor="center")
652
- hw, hh = self.win_w / 2, (self.win_h - PROG_H) / 2
690
+ hw, hh = self.win_w / 2, self.win_h / 2
653
691
  self.zzz = [cv.create_text(cx + hw - 12, cy - hh + 14, text="z",
654
692
  fill="#8A93A6",
655
693
  font=("Segoe UI", 11, "italic bold")),
@@ -671,15 +709,6 @@ class PetApp:
671
709
  for grp in (self.zzz, self.spark):
672
710
  for i in grp:
673
711
  cv.itemconfigure(i, state="hidden")
674
- # 底部进度条(工作且有步骤进度才出现)
675
- self.pbar_bg = cv.create_rectangle(cx - 34, self.win_h - 7,
676
- cx + 34, self.win_h - 2,
677
- fill="#202A36", outline="#3A4654")
678
- self.pbar_fg = cv.create_rectangle(cx - 33, self.win_h - 6,
679
- cx - 32, self.win_h - 3,
680
- fill="#7ED07E", outline="")
681
- cv.itemconfigure(self.pbar_bg, state="hidden")
682
- cv.itemconfigure(self.pbar_fg, state="hidden")
683
712
  self.ox = self.oy = 0.0
684
713
  self._t0 = time.time()
685
714
 
@@ -867,14 +896,6 @@ class PetApp:
867
896
  self._palette = {i: cv.itemcget(i, "fill")
868
897
  for i in (self.body, self.belly)}
869
898
 
870
- # —— 贴地件:进度条(用户拍板去掉影子)——
871
- self.pbar_bg = cv.create_rectangle(46, 139, 128, 147, fill="#202A36",
872
- outline="#3A4654")
873
- self.pbar_fg = cv.create_rectangle(47, 140, 48, 146, fill="#7ED07E",
874
- outline="")
875
- cv.itemconfigure(self.pbar_bg, state="hidden")
876
- cv.itemconfigure(self.pbar_fg, state="hidden")
877
-
878
899
  # 动画游标
879
900
  self.ox = self.oy = 0.0
880
901
  self._t0 = time.time()
@@ -923,26 +944,9 @@ class PetApp:
923
944
  cv.itemconfigure(self.body, fill=self._palette[self.body])
924
945
  cv.itemconfigure(self.belly, fill=self._palette[self.belly])
925
946
  cv.itemconfigure(self.gloss, state="normal")
926
- # —— 共同:Zzz / 星光 / 进度条(徽章与速度线已按用户要求移除)——
947
+ # —— 共同:Zzz / 星光(徽章、速度线、进度条均已按用户要求移除)——
927
948
  self._show(self.zzz, False)
928
949
  self._show(self.spark, False)
929
- prog = []
930
- if st == "work" and self.snap:
931
- prog = [t for t in self.snap["tasks"]
932
- if t["run_status"] == "running" and t["steps_total"]]
933
- if prog:
934
- done = sum(t["steps_done"] for t in prog)
935
- total = sum(t["steps_total"] for t in prog)
936
- ratio = max(0.0, min(1.0, done / float(total)))
937
- x0 = (self.win_w // 2) - 34
938
- cv.itemconfigure(self.pbar_bg, state="normal")
939
- cv.itemconfigure(self.pbar_fg, state="normal")
940
- cv.coords(self.pbar_bg, x0, self.win_h - 7, x0 + 68,
941
- self.win_h - 2)
942
- cv.coords(self.pbar_fg, x0 + 1, self.win_h - 6,
943
- x0 + 1 + 66 * ratio, self.win_h - 3)
944
- else:
945
- self._show([self.pbar_bg, self.pbar_fg], False)
946
950
 
947
951
  def _post_settings(self, body):
948
952
  """合并并串行发送设置,保证快速连点时最后一次选择最终生效。"""
@@ -1029,6 +1033,15 @@ class PetApp:
1029
1033
  else:
1030
1034
  self.miss = 0
1031
1035
  if snap is not None:
1036
+ # 服务换人检测(升级重启同端口场景):boot 与首次见到的不一致,
1037
+ # 说明响应来自新进程——本进程是旧代码的遗老,立即让位,新服务的
1038
+ # 看护会 spawn 带新形象的新蜜蜂(旧形象常驻的根因修复)。
1039
+ boot = snap.get("boot")
1040
+ if boot:
1041
+ if self.boot is None:
1042
+ self.boot = boot
1043
+ elif boot != self.boot:
1044
+ return self._bye()
1032
1045
  self._reconcile_desired_settings(snap, confirmed_at_poll_start)
1033
1046
  if not snap["settings"]["pet_enabled"]:
1034
1047
  return self._bye()
@@ -1375,12 +1388,7 @@ class PetApp:
1375
1388
 
1376
1389
  def _open_ui(self):
1377
1390
  try:
1378
- url = "http://127.0.0.1:%d" % self.port
1379
- # 已开过就不新开:Windows 上先找现有 CodeBee 浏览器窗口置前,
1380
- # 找不到(没开/标签不在前台)才回退 webbrowser.open 新开。
1381
- if os.name == "nt" and _focus_codebee_window(self.root.winfo_id()):
1382
- return
1383
- webbrowser.open(url)
1391
+ openPetWindow(self.port, self.root.winfo_id())
1384
1392
  except Exception:
1385
1393
  pass
1386
1394
 
package/app/ui/app.js CHANGED
@@ -8513,16 +8513,29 @@ function ztHarvestProfiles() {
8513
8513
  });
8514
8514
  }
8515
8515
 
8516
- function ztProfAdd() {
8516
+ async function ztProfAdd() {
8517
8517
  S.ztProfiles = ztHarvestProfiles();
8518
- S.ztProfiles.push({ product: 0, assigned_to: "", severity_cap: 0, our_sides: ["backend"],
8518
+ // 新档案必须来自禅道产品清单。先拉清单,再自动选第一个尚未配置的产品,
8519
+ // 避免生成 product=0 的空卡,用户随后保存才看到“缺产品 ID”。
8520
+ if (!(S.ztProducts || []).length) {
8521
+ if (!($("zt-base-url") && $("zt-base-url").value.trim())) {
8522
+ toast(t("请先测试并保存禅道连接,再添加产品"), true);
8523
+ return;
8524
+ }
8525
+ await ztFetchCatalog(false);
8526
+ S.ztProfiles = ztHarvestProfiles();
8527
+ }
8528
+ const used = new Set(S.ztProfiles.map((p) => String(p.product || "")));
8529
+ const next = (S.ztProducts || []).find((p) => p.id && !used.has(String(p.id)));
8530
+ if (!next) {
8531
+ toast(t((S.ztProducts || []).length ? "禅道产品均已添加" : "未拉到可用产品,请先测试连接"), true);
8532
+ return;
8533
+ }
8534
+ S.ztProfiles.push({ product: Number(next.id), assigned_to: "", severity_cap: 0, our_sides: ["backend"],
8519
8535
  repos: { backend: {}, frontend: {} }, repo_hints: { backend: "", frontend: "" },
8520
8536
  owners: { backend: "", frontend: "", not_ours: "" }, module_routes: [] });
8521
8537
  renderZentaoProfiles();
8522
- // 首次添加时自动拉清单,避免用户只能盲填产品 ID;拉不到仍保留手填回退。
8523
- if (!(S.ztProducts || []).length && $("zt-base-url") && $("zt-base-url").value.trim()) {
8524
- ztFetchCatalog(false);
8525
- }
8538
+ ztFetchMods(S.ztProfiles.length - 1, true);
8526
8539
  }
8527
8540
 
8528
8541
  function ztProfDel(i) {
@@ -8672,6 +8685,13 @@ function ztFormPayload() {
8672
8685
 
8673
8686
  async function saveZentao() {
8674
8687
  const payload = ztFormPayload();
8688
+ if ((payload.product_profiles || []).some((p) => !p.product)) {
8689
+ toast(t("请选择禅道产品后再保存"), true);
8690
+ const empty = Array.from(document.querySelectorAll("#zt-profiles .zt-p-product"))
8691
+ .find((el) => !parseInt(el.value || "", 10));
8692
+ if (empty) empty.focus();
8693
+ return;
8694
+ }
8675
8695
  try {
8676
8696
  await api("/api/zentao/config", { method: "POST", body: JSON.stringify(payload) });
8677
8697
  } catch (e) { toast(t("保存失败:") + e.message, true); return; }
@@ -10601,6 +10621,11 @@ function collapseDrawerIfMobile() {
10601
10621
  function switchTab(name) {
10602
10622
  if (name === "__phone") { openPhoneConnect(); return; } // 手机连接是弹框,不切页
10603
10623
  if (name === "__guide") { welcomeOpen(); return; } // 帮助中心是弹层,不切页(设置导航「软件」组)
10624
+ if (name === "__wxdigest") { // 群聊汇报沿用全局蜜蜂面板
10625
+ beeRefresh().finally(() => beeToggle(true));
10626
+ collapseDrawerIfMobile();
10627
+ return;
10628
+ }
10604
10629
  // 导航收进「设置」:进设置后左栏整体换成设置导航,内容铺满
10605
10630
  S.tab = name;
10606
10631
  if (SET_TABS.has(name)) localStorage.setItem("orch.setTab", name);
@@ -11314,7 +11339,7 @@ function bindCmdK() {
11314
11339
 
11315
11340
  /* ------------------------------------------------ 群摘要蜜蜂坞(core/wxdigest.py)
11316
11341
  * 右下角悬浮蜜蜂:未读摘要徽章+气泡;面板看各群摘要、配置监控文件夹。
11317
- * 桌面蜜蜂 pet.py 的网页侧搭档;未启用且无数据时整坞隐藏(不占视线)。
11342
+ * 桌面蜜蜂 pet.py 的网页侧搭档;入口始终可见,未配置时也能直接打开设置。
11318
11343
  * 60s 轮询 /api/wxdigest;打开面板即 POST seen 清零未读。 */
11319
11344
  const Bee = { view: null, lastUnseen: 0, bubT: 0, selGroups: null };
11320
11345
 
@@ -11325,9 +11350,7 @@ async function beeRefresh() {
11325
11350
  let v;
11326
11351
  try { v = await api("/api/wxdigest"); } catch (e) { return; } // 静默:不打扰主流程
11327
11352
  Bee.view = v;
11328
- const on = !!(v.config && v.config.enabled) ||
11329
- (v.digests && v.digests.length) || v.unseen > 0;
11330
- beeShow($("bee-dock"), on);
11353
+ beeShow($("bee-dock"), true);
11331
11354
  beeShow($("bee-badge"), v.unseen > 0);
11332
11355
  if (v.unseen > 0) $("bee-badge").textContent = v.unseen > 99 ? "99+" : String(v.unseen);
11333
11356
  if (v.unseen > Bee.lastUnseen && !beePanelOpen()) { // 新摘要到:气泡 6s
package/app/ui/i18n.js CHANGED
@@ -2005,6 +2005,10 @@
2005
2005
  "还没有认领过 Bug——配置好连接与产品档案后点「立即扫描」。": "No bugs claimed yet — set up connection and product profiles, then click \"Scan Now\".",
2006
2006
  "还没有产品档案——点「+ 添加产品」,填产品 ID 与两端仓库。": "No product profiles yet — click \"+ Add product\" and fill the product ID and repos.",
2007
2007
  "还没有产品档案——先测试连接,再从禅道选择产品,无需自己查 ID。": "No product profiles yet — test the connection, then select a product from ZenTao; no ID lookup needed.",
2008
+ "请先测试并保存禅道连接,再添加产品": "Test and save the ZenTao connection before adding a product.",
2009
+ "禅道产品均已添加": "All ZenTao products have already been added.",
2010
+ "未拉到可用产品,请先测试连接": "No available products were found. Test the connection first.",
2011
+ "请选择禅道产品后再保存": "Select a ZenTao product before saving.",
2008
2012
  "先填产品 ID 再拉模块清单": "Fill the product ID before fetching modules",
2009
2013
  "先选产品再拉模块清单": "Pick a product before fetching modules",
2010
2014
  "拉取失败": "Fetch failed",
@@ -2200,6 +2204,7 @@
2200
2204
 
2201
2205
  // —— 群摘要蜜蜂坞(wxdigest)——
2202
2206
  "群摘要": "Group Digests",
2207
+ "微信群聊汇报": "WeChat Group Reports",
2203
2208
  "启用定时扫描": "Enable scheduled scanning",
2204
2209
  "监控文件夹": "Watched folder",
2205
2210
  "间隔(分钟)": "Interval (min)",
package/app/ui/index.html CHANGED
@@ -178,6 +178,7 @@
178
178
  <button class="set-item active" data-sub="tasks" data-i18n="任务"><svg class="ico" aria-hidden="true"><use href="#i-tasks"></use></svg>任务</button>
179
179
  <button class="set-item" data-sub="runs" data-i18n="运行记录"><svg class="ico" aria-hidden="true"><use href="#i-history"></use></svg>运行记录</button>
180
180
  <button class="set-item" data-sub="automation" data-i18n="自动化"><svg class="ico" aria-hidden="true"><use href="#i-calendar-days"></use></svg>自动化</button>
181
+ <button class="set-item" data-sub="__wxdigest" data-i18n="微信群聊汇报"><svg class="ico" aria-hidden="true"><use href="#i-bee"></use></svg>微信群聊汇报</button>
181
182
  <button class="set-item" data-sub="zentao" data-i18n="禅道"><svg class="ico" aria-hidden="true"><use href="#i-search"></use></svg>禅道</button>
182
183
  <button class="set-item" data-sub="usage" data-i18n="用量统计"><svg class="ico" aria-hidden="true"><use href="#i-chart"></use></svg>用量统计</button>
183
184
  <div data-i18n="配置" class="set-group">配置</div>
@@ -557,12 +558,12 @@
557
558
  <label data-i18n="密码">密码</label><input id="zt-password" type="password" placeholder="" autocomplete="new-password">
558
559
  </div>
559
560
  <h3 data-i18n="自动化" class="sec-title">自动化</h3>
560
- <div class="zt-grid zt-grid-toggles">
561
+ <div class="zt-grid zt-grid-toggles zt-auto-options">
561
562
  <label class="toggle" title="模块路由没命中时,用 AI 读 Bug 描述判端(前端/后端/双端/非我方);关掉则未命中模块的 Bug 留人工" data-i18n-title="模块路由没命中时,用 AI 读 Bug 描述判端(前端/后端/双端/非我方);关掉则未命中模块的 Bug 留人工"><input id="zt-triageai" type="checkbox"><span data-i18n="AI 排查兜底">AI 排查兜底</span></label>
562
563
  <label class="toggle" title="修复任务评审通过后,自动到禅道 resolve(fixed)并回评论" data-i18n-title="修复任务评审通过后,自动到禅道 resolve(fixed)并回评论"><input id="zt-autoresolve" type="checkbox"><span data-i18n="自动 resolve Bug">自动 resolve Bug</span></label>
563
564
  <label class="toggle" title="把任务分支自动合并回基线分支(需配置基线分支;合并失败不 resolve 不转派,留人工)" data-i18n-title="把任务分支自动合并回基线分支(需配置基线分支;合并失败不 resolve 不转派,留人工)"><input id="zt-automerge" type="checkbox"><span data-i18n="自动合并代码">自动合并代码</span></label>
564
565
  <label class="toggle" title="按下面的间隔定时扫描;关掉后仍可手动「立即扫描」" data-i18n-title="按下面的间隔定时扫描;关掉后仍可手动「立即扫描」"><input id="zt-poll" type="checkbox"><span data-i18n="定时扫描">定时扫描</span></label>
565
- <label data-i18n="扫描间隔(小时)">扫描间隔(小时)</label><input id="zt-interval" type="number" min="1" max="168" step="1">
566
+ <label class="zt-interval-field"><span data-i18n="扫描间隔(小时)">扫描间隔(小时)</span><input id="zt-interval" type="number" min="1" max="168" step="1"></label>
566
567
  </div>
567
568
  <div class="ops"><button id="btn-zentao-save" class="primary small"><span data-i18n="保存配置">保存配置</span></button></div>
568
569
  <h3 data-i18n="产品档案" class="sec-title">产品档案</h3>
@@ -1107,10 +1108,10 @@
1107
1108
 
1108
1109
  <!-- 群摘要蜜蜂坞(右下角悬浮蜜蜂):桌面蜜蜂 pet.py 的网页侧搭档。
1109
1110
  未读摘要时徽章+气泡提醒,点开面板看各群摘要/配置监控文件夹;
1110
- 未启用且无数据时整坞隐藏(不占视线)。后端 core/wxdigest.py。 -->
1111
- <div id="bee-dock" class="hidden">
1111
+ 未配置时也保持入口可见,避免功能存在却无处开启。后端 core/wxdigest.py。 -->
1112
+ <div id="bee-dock">
1112
1113
  <div id="bee-bubble" class="hidden"></div>
1113
- <button id="bee-fab" aria-label="群摘要" data-i18n-aria="群摘要" data-i18n-title="群摘要" title="群摘要">
1114
+ <button id="bee-fab" aria-label="微信群聊汇报" data-i18n-aria="微信群聊汇报" data-i18n-title="微信群聊汇报" title="微信群聊汇报">
1114
1115
  <svg class="ico ico-lg" aria-hidden="true"><use href="#i-bee"></use></svg>
1115
1116
  <span id="bee-badge" class="hidden"></span>
1116
1117
  </button>
package/app/ui/style.css CHANGED
@@ -5521,3 +5521,34 @@ button.request-busy > * { opacity: .18; }
5521
5521
  line-height: 1.6;
5522
5522
  }
5523
5523
  #su-info.empty { text-align: center; }
5524
+
5525
+ /* ---------------- 禅道·添加产品与自动化布局(追加于文件尾) ---------------- */
5526
+ .zt-prof .head .name {
5527
+ display: flex; flex: 1 1 280px; min-width: 0; align-items: center; gap: 8px;
5528
+ overflow: visible; white-space: normal;
5529
+ }
5530
+ .zt-prof .head .zt-p-product {
5531
+ width: min(360px, 100%); min-width: 180px; flex: 1 1 240px;
5532
+ }
5533
+ .zt-auto-options {
5534
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
5535
+ gap: 12px 18px; align-items: start;
5536
+ }
5537
+ @media (min-width: 901px) {
5538
+ .zt-auto-options { grid-template-columns: repeat(5, minmax(0, 1fr)); }
5539
+ }
5540
+ .zt-auto-options > .toggle {
5541
+ min-height: 38px; margin: 0; align-self: end;
5542
+ }
5543
+ .zt-auto-options .zt-interval-field {
5544
+ display: grid; grid-template-columns: 1fr; gap: 6px; min-width: 0;
5545
+ color: var(--muted); font-size: 12.5px;
5546
+ }
5547
+ .zt-auto-options .zt-interval-field input {
5548
+ width: 100%; max-width: none; box-sizing: border-box;
5549
+ }
5550
+ @media (max-width: 640px) {
5551
+ .zt-auto-options { grid-template-columns: 1fr 1fr; }
5552
+ .zt-auto-options .zt-interval-field { grid-column: 1 / -1; }
5553
+ .zt-prof .head .zt-p-product { min-width: 0; }
5554
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codebee",
3
- "version": "0.1.28",
3
+ "version": "0.1.29",
4
4
  "description": "CodeBee · 多智能体编排台 — 统一编排本机 AI 编码 CLI(Codex / Claude Code / QwenCode / OpenCode / Aider…):目标→拆解→路由→执行→验证→跨厂商评审→修复→报告。纯 Python 标准库实现,本地 Web UI。",
5
5
  "license": "MIT",
6
6
  "bin": {