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/CHANGELOG.md CHANGED
@@ -6,6 +6,23 @@ README 元数据带回,供老版本在「发现新版本」时展示新版更
6
6
 
7
7
  ## 未发布
8
8
 
9
+ ## v0.1.32(2026-09-21)
10
+
11
+ ### ✨ 新功能
12
+
13
+ - 连载任务详情新增「章节评审」卡片:每章的维度评分(低分标红)、达标状态、字数与修订轮数一目了然;每章按大纲逐项核对的判定结果(已完成/未完成/待核实)以红绿灰行直接展示——哪章哪个要点没写到一眼可查。
14
+
15
+ ## v0.1.31(2026-09-21)
16
+
17
+ ### ✨ 新功能
18
+
19
+ - 群摘要搬进正式页面:设置 →「微信群聊汇报」现在是完整页面——配置(监控文件夹/微信直连/Python 扫描与一键安装/监听群)和各群摘要列表一体呈现,不再挤在右下角悬浮小窗里。
20
+ - 桌面蜜蜂右键「群摘要」直达该页面(已打开的页面只聚焦不重复开)。
21
+
22
+ ### 🛠 问题修复
23
+
24
+ - 修复微信直连「选不中 Python」:此前每分钟的自动刷新会把用户刚选好但还没保存的路径悄悄冲回旧值,现在改过路径后不再回填覆盖,保存成功后才更新。
25
+
9
26
  ## v0.1.30(2026-09-21)
10
27
 
11
28
  ### ✨ 新功能
package/README.md CHANGED
@@ -23,6 +23,17 @@ Kimi Code、MiMo Code、Grok Build、Pi、DeepSeek Harness……),提供
23
23
  <!-- relnotes:start -->
24
24
  ### ✨ 新功能 · New
25
25
 
26
+ - 连载任务详情新增「章节评审」卡片:每章维度评分(低分标红)与达标状态,大纲要点逐项核对结果红绿灰直读。
27
+ - Serial runs now show a Chapter Reviews card: per-chapter dimension scores (low ones in red) plus beat-by-beat checks color-coded green/red/gray.
28
+
29
+ - 群摘要搬进正式页面:设置 →「微信群聊汇报」里配置与摘要列表一体呈现,右下角悬浮小窗已移除;桌面蜜蜂右键「群摘要」直达。
30
+ - Group digests now live on a proper settings page (config + digest list in one place); the floating bottom-right widget is gone, and the desktop bee links straight to it.
31
+
32
+ ### 🛠 问题修复 · Fixes
33
+
34
+ - 修复「选不中 Python」:自动刷新不再冲掉你刚选好但还没保存的路径。
35
+ - Fixed "can't pick a Python": background refresh no longer wipes a freshly chosen but unsaved path.
36
+
26
37
  - 微信直连配置更省事:「扫描」自动找本机 Python(标注位数与依赖状态,点选即用),选中后可一键安装 wechatauto-replica。
27
38
  - WeChat setup is now effortless: a Scan button finds local Pythons (with bitness and dependency badges), and one click installs wechatauto-replica.
28
39
 
@@ -566,7 +566,9 @@ def run(bi, prompt, workdir, timeout=180, cancel_event=None, log=None, images=No
566
566
  url, headers, body = _build_request(proto, pbase, kk["key"], model,
567
567
  system, msgs, tools_ok)
568
568
  effort = str(bi.get("reasoning_effort") or "").strip().lower()
569
- if effort in ("low", "medium", "high") and proto != "google":
569
+ # reasoning_effort 是 OpenAI wire 字段;Anthropic thinking 使用
570
+ # 另一套对象结构,向兼容网关硬塞该字段会直接得到 400。
571
+ if effort in ("low", "medium", "high") and proto == "openai":
570
572
  body["reasoning_effort"] = effort
571
573
  status, data, err = _post_interruptible(url, headers, body,
572
574
  allow_private, timeout,
@@ -3003,16 +3003,17 @@ def resolve_orchestrator():
3003
3003
  return prov, model
3004
3004
 
3005
3005
 
3006
- def _chat_cache_path(provider_id, model_name, prompt, max_tokens):
3006
+ def _chat_cache_path(provider_id, model_name, prompt, max_tokens, reasoning_effort=""):
3007
3007
  """§07 T2.2:精确匹配响应缓存的落盘路径(只缓存 ok 的幂等调用)。"""
3008
3008
  import hashlib as _h
3009
- key = "|".join([str(provider_id), str(model_name), str(max_tokens), str(prompt)])
3009
+ key = "|".join([str(provider_id), str(model_name), str(max_tokens),
3010
+ str(reasoning_effort or ""), str(prompt)])
3010
3011
  name = _h.sha256(key.encode("utf-8")).hexdigest()[:24]
3011
3012
  return paths.DATA_DIR / "chat_cache" / (name + ".json")
3012
3013
 
3013
3014
 
3014
3015
  def chat(provider_id, model_name, prompt, max_tokens=2048, timeout=120, cache_ttl=0,
3015
- on_delta=None):
3016
+ on_delta=None, reasoning_effort=""):
3016
3017
  """直连供应商 API 做一次对话(编排者规划 / 连通性测试)。
3017
3018
 
3018
3019
  支持 anthropic / openai / google 三种协议;复用 SSRF 防护。
@@ -3026,7 +3027,8 @@ def chat(provider_id, model_name, prompt, max_tokens=2048, timeout=120, cache_tt
3026
3027
  """
3027
3028
  if cache_ttl > 0:
3028
3029
  try:
3029
- cache_path = _chat_cache_path(provider_id, model_name, prompt, max_tokens)
3030
+ cache_path = _chat_cache_path(provider_id, model_name, prompt, max_tokens,
3031
+ reasoning_effort)
3030
3032
  if cache_path.is_file():
3031
3033
  age = time.time() - cache_path.stat().st_mtime
3032
3034
  if age <= cache_ttl:
@@ -3073,6 +3075,9 @@ def chat(provider_id, model_name, prompt, max_tokens=2048, timeout=120, cache_tt
3073
3075
  headers = {"Authorization": "Bearer " + use_key}
3074
3076
  body = {"model": model_name, "max_tokens": max_tokens,
3075
3077
  "messages": [{"role": "user", "content": prompt}]}
3078
+ effort = str(reasoning_effort or "").strip().lower()
3079
+ if proto == "openai" and effort in ("low", "medium", "high"):
3080
+ body["reasoning_effort"] = effort
3076
3081
  return url, headers, body
3077
3082
 
3078
3083
  last_err = ""
@@ -3156,7 +3161,8 @@ def chat(provider_id, model_name, prompt, max_tokens=2048, timeout=120, cache_tt
3156
3161
  # §07 T2.2:cache_ttl>0 时落盘缓存(仅 ok 结果,原子写)
3157
3162
  if cache_ttl > 0:
3158
3163
  try:
3159
- cache_path = _chat_cache_path(provider_id, model_name, prompt, max_tokens)
3164
+ cache_path = _chat_cache_path(provider_id, model_name, prompt, max_tokens,
3165
+ reasoning_effort)
3160
3166
  cache_path.parent.mkdir(parents=True, exist_ok=True)
3161
3167
  tmp = cache_path.with_suffix(".tmp")
3162
3168
  import json as _json
@@ -961,7 +961,7 @@ def _run_code(run, task, agents, ev, stats, mode):
961
961
  and not task.get("attachments")
962
962
  and user_context_len < 600))
963
963
  planned = False
964
- if mode == "auto" and not fast_path:
964
+ if mode in ("auto", "expert") and not fast_path:
965
965
  _wait_gate(run_id, ev)
966
966
  plan_step, plan_log = store.add_step(run_id, "plan", impl["id"], impl.get("label"),
967
967
  note=route.get("implementer", ""))
@@ -1003,7 +1003,7 @@ def _run_code(run, task, agents, ev, stats, mode):
1003
1003
  if not workflow["review_required"]:
1004
1004
  reviewer = None
1005
1005
  route["reviewer"] = workflow["reason"]
1006
- elif mode == "auto":
1006
+ elif mode in ("auto", "expert"):
1007
1007
  reviewer, route["reviewer"] = router.pick_reviewer(agents, impl, "code", stats)
1008
1008
  else:
1009
1009
  reviewer, route["reviewer"] = _pick_reviewer_legacy(agents, impl)
@@ -1235,7 +1235,8 @@ def _run_code(run, task, agents, ev, stats, mode):
1235
1235
  "# 代码任务报告:%s" % task["title"], "",
1236
1236
  "- 结论:**%s**" % ("✅ 通过" if overall_pass else "❌ 未通过"),
1237
1237
  "- 编排模式:%s 实现者:%s 评审者:%s" % (
1238
- "智能" if mode == "auto" else "手动", impl.get("label"),
1238
+ {"auto": "自动", "fast": "快速", "expert": "专家",
1239
+ "manual": "手动"}.get(mode, mode), impl.get("label"),
1239
1240
  reviewer.get("label") if reviewer else "按验证结果省略"),
1240
1241
  "- 动态步骤:规划 %s;实现 %d 步;验证 %s;评审 %d 人;最多修复 %d 轮;换将 %s" % (
1241
1242
  "独立执行" if workflow["planning"] == "llm" else "并入实现",
@@ -2941,7 +2942,8 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
2941
2942
  "通过" if chapters_pass else "未通过",
2942
2943
  "通过" if global_pass else "未通过"),
2943
2944
  "- 编排模式:%s 作者:%s 评审组:%s" % (
2944
- "智能" if mode == "auto" else "手动",
2945
+ {"auto": "自动", "fast": "快速", "expert": "专家",
2946
+ "manual": "手动"}.get(mode, mode),
2945
2947
  impl.get("label"), "、".join(a.get("label") for a in critics)),
2946
2948
  "", "## 各章得分(章阈值 %.1f)" % threshold_ch, "",
2947
2949
  "| 章 | 标题 | " + " | ".join(dims) + " | 均分 | 达标 | 轮次 | 字数 |",
@@ -3119,7 +3121,7 @@ def _run_content_review(run, task, agents, ev, stats, mode):
3119
3121
  error="没有可用智能体", ended_at=_now())
3120
3122
  return
3121
3123
  if resume_ctx is not None:
3122
- if mode == "auto":
3124
+ if mode != "manual":
3123
3125
  critics, route["critics"] = router.pick_critics(
3124
3126
  agents, task.get("type") or "novel", stats, impl=impl)
3125
3127
  else:
@@ -3151,7 +3153,7 @@ def _run_content_review(run, task, agents, ev, stats, mode):
3151
3153
  except Exception:
3152
3154
  return ""
3153
3155
 
3154
- draft_note = route.get("author", "") if mode == "auto" else ""
3156
+ draft_note = route.get("author", "") if mode != "manual" else ""
3155
3157
 
3156
3158
  # 编排者大纲:只对真实执行有意义;失败静默退回无大纲(喂入带指令的任务副本)
3157
3159
  outline = (planner.make_review_outline(_steered_task(run_id, task))
@@ -327,8 +327,9 @@ def make_serial_outline(task, author_agent=None, workdir=None, ev=None, log_path
327
327
  # 否则 stop_reason=max_tokens、正文为空(实测 2048 全被思考吞掉)
328
328
  # 流式回调把增量实时写进步骤日志,直连生成不再是一行标题的黑箱
329
329
  cb = _log_streamer(log_path)
330
- res = modelhub.chat(prov["id"], model, prompt,
331
- max_tokens=16000, timeout=300, on_delta=cb)
330
+ res = modelhub.chat(prov["id"], model, prompt,
331
+ max_tokens=16000, timeout=300, on_delta=cb,
332
+ reasoning_effort=_reasoning_effort(task))
332
333
  cb.flush()
333
334
  _log_usage("outline", "outline", task, res, model=model,
334
335
  provider=prov.get("name", prov.get("id", "")),
@@ -431,12 +432,33 @@ def _plan_workflow(data):
431
432
  return out or None
432
433
 
433
434
 
434
- def _orchestrator():
435
- """编排者可用时返回 (provider, model),否则 None。"""
436
- try:
437
- return modelhub.resolve_orchestrator()
438
- except Exception:
439
- return None
435
+ def _orchestrator():
436
+ """编排者可用时返回 (provider, model),否则 None。"""
437
+ try:
438
+ return modelhub.resolve_orchestrator()
439
+ except Exception:
440
+ return None
441
+
442
+
443
+ def _reasoning_effort(task):
444
+ """把任务级思考程度映射为直连编排者支持的 reasoning 参数。"""
445
+ task = task if isinstance(task, dict) else {}
446
+ selected = str(task.get("thinking") or "standard").strip().lower()
447
+ if selected in ("low", "standard", "high"):
448
+ return {"low": "low", "standard": "medium", "high": "high"}[selected]
449
+ mode = str(task.get("mode") or "auto").lower()
450
+ difficulty = str(task.get("difficulty") or "auto").lower()
451
+ ttype = str(task.get("type") or "").lower()
452
+ try:
453
+ threshold = float(task.get("threshold") or 7.0)
454
+ except (TypeError, ValueError):
455
+ threshold = 7.0
456
+ if mode == "fast" or difficulty == "easy" or threshold <= 6.0:
457
+ return "low"
458
+ if (mode == "expert" or difficulty == "hard" or threshold >= 8.5
459
+ or ttype in ("research", "tech_proposal", "serial_novel")):
460
+ return "high"
461
+ return "medium"
440
462
 
441
463
 
442
464
  def make_code_plan(task, planner_agent, workdir, ev=None, resume=None, log_path=None):
@@ -479,7 +501,8 @@ def _orch_code_plan(task, prov, model, log_path=None):
479
501
  .replace("__GOAL__", task["goal"])
480
502
  .replace("__CONTEXT__", task.get("context") or "(无)")
481
503
  .replace("__VERIFY__", task.get("verify_command") or "(未配置)")),
482
- max_tokens=8000, timeout=300, on_delta=cb)
504
+ max_tokens=8000, timeout=300, on_delta=cb,
505
+ reasoning_effort=_reasoning_effort(task))
483
506
  cb.flush()
484
507
  _log_usage("plan", "plan", task, res, model=model,
485
508
  provider=prov.get("name", prov.get("id", "")))
@@ -507,7 +530,8 @@ def make_review_outline(task):
507
530
  (REVIEW_OUTLINE_PROMPT
508
531
  .replace("__GOAL__", task["goal"])
509
532
  .replace("__CONTEXT__", task.get("context") or "(无)")),
510
- max_tokens=8000, timeout=300)
533
+ max_tokens=8000, timeout=300,
534
+ reasoning_effort=_reasoning_effort(task))
511
535
  _log_usage("outline", "outline", task, res, model=model,
512
536
  provider=prov.get("name", prov.get("id", "")))
513
537
  if not res["ok"]:
@@ -6,7 +6,7 @@
6
6
  监听 tools/post-execute,按 (tool, canonical_args) 跟踪,3/5/8 阈值注入 user-message 提醒。
7
7
 
8
8
  Tutti 的 "tool" 对应到 "step role","canonical_args" 对应到 prompt 模板指纹。
9
- 阈值更宽松(5/10/15),因 Tutti 步骤级别比 dsh 工具调用粗。
9
+ 步骤级调用代价远高于普通工具调用,因此第 3 次提醒、第 5 次强制停止。
10
10
  """
11
11
  from __future__ import annotations
12
12
 
@@ -18,7 +18,7 @@ import time
18
18
  log = logging.getLogger(__name__)
19
19
 
20
20
  # 渐进阈值:第 N 次命中时给提醒;最后一次及以上时强制停止。
21
- DEFAULT_THRESHOLDS = (5, 10, 15)
21
+ DEFAULT_THRESHOLDS = (3, 5)
22
22
 
23
23
  REMINDER_TEMPLATE = (
24
24
  "⚠️ 提醒:上一步使用了几乎相同的输入(已连续 {count} 次调用 {role})。"
@@ -121,4 +121,4 @@ class RepeatGuard:
121
121
 
122
122
 
123
123
  # 模块级默认实例(pipeline 接线用;run_id 入键天然隔离多任务,测试无需替换)
124
- guard = RepeatGuard()
124
+ guard = RepeatGuard()
package/app/core/store.py CHANGED
@@ -486,6 +486,15 @@ def load_all():
486
486
 
487
487
  # ---------------------------------------------------------------- 运行(含管理操作)
488
488
 
489
+ _RUN_ESTIMATOR = None
490
+
491
+
492
+ def set_run_estimator(estimator):
493
+ """由应用入口注入 ETA 估算器,保持存储层不反向依赖用量模块。"""
494
+ global _RUN_ESTIMATOR
495
+ _RUN_ESTIMATOR = estimator if callable(estimator) else None
496
+
497
+
489
498
  def create_run(kind, title, task_id=None, entry_id=None, op=None):
490
499
  run = {
491
500
  "id": _new_id("r" if kind == "orchestration" else "m"),
@@ -501,8 +510,7 @@ def create_run(kind, title, task_id=None, entry_id=None, op=None):
501
510
  if kind == "orchestration" and task_id:
502
511
  task = get_task(task_id) or {}
503
512
  try:
504
- from . import usage
505
- eta = usage.estimate(
513
+ eta = _RUN_ESTIMATOR(
506
514
  task_type=task.get("type") or "", mode=task.get("mode") or "auto",
507
515
  thinking=task.get("thinking") or "standard",
508
516
  rounds=task.get("rounds"))
@@ -103,8 +103,9 @@ def code_workflow(task, difficulty, plan=None, mode="auto", planned=True):
103
103
  risk_text = " ".join(str(task.get(k) or "") for k in ("title", "goal", "context")).lower()
104
104
  high_risk = bool(any(k in risk_text for k in (
105
105
  "安全", "权限", "鉴权", "认证", "加密", "密钥", "注入", "迁移", "数据库",
106
- "并发", "竞态", "支付", "public api", "公共 api", "breaking change",
107
- "security", "permission", "auth", "migration", "concurrency")))
106
+ "并发", "竞态", "支付", "架构", "公共接口", "public api", "公共 api",
107
+ "breaking change", "security", "permission", "auth", "migration",
108
+ "concurrency", "architecture")))
108
109
  fast_mode = mode == "fast"
109
110
  expert_mode = mode == "expert"
110
111
  easy_auto = (mode == "auto" and difficulty == "easy") or fast_mode
package/app/main.py CHANGED
@@ -2386,6 +2386,7 @@ def main():
2386
2386
  except Exception:
2387
2387
  pass
2388
2388
  from core import usage
2389
+ store.set_run_estimator(usage.estimate)
2389
2390
  _step("正在回填用量台账…")
2390
2391
  n_bf = usage.backfill_from_runs() # 历史运行 token 回填台账(幂等,仅补缺失步骤)
2391
2392
  if n_bf:
@@ -2536,7 +2537,8 @@ def main():
2536
2537
 
2537
2538
  print("[CodeBee] 本机 http://127.0.0.1:%d" % args.port, flush=True)
2538
2539
  print("[CodeBee] 数据目录 %s" % paths.DATA_DIR, flush=True)
2539
- print("[CodeBee] ✔ 已就绪,浏览器即将自动打开;不要关闭本窗口。", flush=True)
2540
+ # Windows 的默认 GBK 控制台无法编码部分符号,启动日志必须保持控制台安全。
2541
+ print("[CodeBee] [OK] 已就绪,浏览器即将自动打开;不要关闭本窗口。", flush=True)
2540
2542
  try:
2541
2543
  import faulthandler
2542
2544
  faulthandler.cancel_dump_traceback_later() # 启动完成,看门狗退役(否则运行期每 20s 误报堆栈)