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/CHANGELOG.md CHANGED
@@ -6,6 +6,30 @@ README 元数据带回,供老版本在「发现新版本」时展示新版更
6
6
 
7
7
  ## 未发布
8
8
 
9
+ ## v0.1.31(2026-09-21)
10
+
11
+ ### ✨ 新功能
12
+
13
+ - 群摘要搬进正式页面:设置 →「微信群聊汇报」现在是完整页面——配置(监控文件夹/微信直连/Python 扫描与一键安装/监听群)和各群摘要列表一体呈现,不再挤在右下角悬浮小窗里。
14
+ - 桌面蜜蜂右键「群摘要」直达该页面(已打开的页面只聚焦不重复开)。
15
+
16
+ ### 🛠 问题修复
17
+
18
+ - 修复微信直连「选不中 Python」:此前每分钟的自动刷新会把用户刚选好但还没保存的路径悄悄冲回旧值,现在改过路径后不再回填覆盖,保存成功后才更新。
19
+
20
+ ## v0.1.30(2026-09-21)
21
+
22
+ ### ✨ 新功能
23
+
24
+ - 微信直连配置更省事:新增「扫描」按钮自动找出本机全部 Python(标注位数与是否已装依赖,点选即回填);选中未装依赖的 64 位解释器可「一键安装 wechatauto-replica」,不用再自己开 conda 敲命令。
25
+ - 桌面蜜蜂右键新增「群摘要」:直达群摘要面板(已打开的页面只聚焦不重复开)。
26
+ - 直连任务可选指定厂商与模型。
27
+
28
+ ### 🛠 问题修复
29
+
30
+ - 修复没装 Pillow 的电脑上蜜蜂永远显示手绘丑蜜蜂:现在用系统自带能力直接显示新形象贴图(普通用户升级即变好看,无需装任何额外依赖)。
31
+ - 禅道自动化四个开关与扫描间隔紧凑排成一行。
32
+
9
33
  ## v0.1.29(2026-09-21)
10
34
 
11
35
  ### ✨ 新功能
package/README.md CHANGED
@@ -23,11 +23,33 @@ Kimi Code、MiMo Code、Grok Build、Pi、DeepSeek Harness……),提供
23
23
  <!-- relnotes:start -->
24
24
  ### ✨ 新功能 · New
25
25
 
26
- - 升级后蜜蜂自动换新装:服务重启后旧蜜蜂自动让位,新蜜蜂带新形象上线。
27
- - After upgrading, the desktop bee swaps itself for the new look: the old bee steps aside when the service restarts.
26
+ - 群摘要搬进正式页面:设置 →「微信群聊汇报」里配置与摘要列表一体呈现,右下角悬浮小窗已移除;桌面蜜蜂右键「群摘要」直达。
27
+ - 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.
28
+
29
+ ### 🛠 问题修复 · Fixes
30
+
31
+ - 修复「选不中 Python」:自动刷新不再冲掉你刚选好但还没保存的路径。
32
+ - Fixed "can't pick a Python": background refresh no longer wipes a freshly chosen but unsaved path.
33
+
34
+ - 微信直连配置更省事:「扫描」自动找本机 Python(标注位数与依赖状态,点选即用),选中后可一键安装 wechatauto-replica。
35
+ - WeChat setup is now effortless: a Scan button finds local Pythons (with bitness and dependency badges), and one click installs wechatauto-replica.
36
+
37
+ - 桌面蜜蜂右键新增「群摘要」:直达群摘要面板,已打开的页面只聚焦不重复开。
38
+ - The desktop bee's right-click menu now has "Group Digests" — it focuses the existing page instead of opening a duplicate.
39
+
40
+ - 没装 Pillow 的电脑上蜜蜂也会显示新形象贴图了(修复手绘丑蜜蜂问题)。
41
+ - Bees now show the new artwork even without Pillow installed (fixes the ugly hand-drawn fallback).
28
42
 
29
43
  ### 🛠 问题修复 · Fixes
30
44
 
45
+ - 禅道自动化开关与扫描间隔紧凑排成一行。
46
+ - ZenTao automation toggles and the scan interval now fit on one tidy row.
47
+
48
+ ### 上一版亮点 · Previous release
49
+
50
+ - 升级后蜜蜂自动换新装:服务重启后旧蜜蜂自动让位,新蜜蜂带新形象上线。
51
+ - After upgrading, the desktop bee swaps itself for the new look: the old bee steps aside when the service restarts.
52
+
31
53
  - 去掉蜜蜂底部的任务进度条,窗口更干净。
32
54
  - Removed the task progress bar under the desktop bee for a cleaner look.
33
55
 
@@ -39,11 +39,40 @@ _SYSTEM_PROMPT = """你是 CodeBee 的内置执行智能体,直接完成用户
39
39
  - 回答用户的语言与用户一致(默认中文)。直接给结论和内容,不要输出任何机器标记或协议行。"""
40
40
 
41
41
 
42
- def resolve():
42
+ def resolve(provider_id="", model="", difficulty="default"):
43
43
  """选内置智能体可用的 (provider, model):编排者配置优先,否则首个可用供应商。
44
44
 
45
45
  返回 {prov, model, provider_id, provider_name} 或 None(无可用供应商 → 调用方
46
46
  回退 CLI 路径)。"""
47
+ provider_id = str(provider_id or "").strip()
48
+ model = str(model or "").strip()
49
+ if provider_id:
50
+ for p in modelhub.providers():
51
+ if p.get("id") != provider_id or not p.get("enabled", True) or not p.get("api_key"):
52
+ continue
53
+ selected = model
54
+ if selected:
55
+ try:
56
+ enabled_names = [m.get("name") for m in modelhub._enabled_models(p)]
57
+ except Exception:
58
+ enabled_names = []
59
+ if selected not in enabled_names:
60
+ return None
61
+ if not selected:
62
+ selected = (p.get("model_hard") if difficulty == "hard" else
63
+ p.get("model_easy") if difficulty == "easy" else
64
+ p.get("model") or "").strip()
65
+ if not selected:
66
+ try:
67
+ names = modelhub._enabled_models(p)
68
+ except Exception:
69
+ names = []
70
+ selected = names[0]["name"] if names else ""
71
+ if selected:
72
+ return {"prov": p, "model": selected,
73
+ "provider_id": p.get("id") or "",
74
+ "provider_name": p.get("name") or p.get("id") or ""}
75
+ return None
47
76
  orch = None
48
77
  try:
49
78
  orch = modelhub.resolve_orchestrator()
@@ -57,7 +86,9 @@ def resolve():
57
86
  for p in modelhub.providers():
58
87
  if not p.get("enabled", True) or not p.get("api_key"):
59
88
  continue
60
- m = (p.get("model") or "").strip()
89
+ m = ((p.get("model_hard") if difficulty == "hard" else
90
+ p.get("model_easy") if difficulty == "easy" else
91
+ p.get("model")) or "").strip()
61
92
  if not m:
62
93
  try:
63
94
  names = modelhub._enabled_models(p)
@@ -534,6 +565,11 @@ def run(bi, prompt, workdir, timeout=180, cancel_event=None, log=None, images=No
534
565
  for kk in keys:
535
566
  url, headers, body = _build_request(proto, pbase, kk["key"], model,
536
567
  system, msgs, tools_ok)
568
+ effort = str(bi.get("reasoning_effort") or "").strip().lower()
569
+ # reasoning_effort 是 OpenAI wire 字段;Anthropic thinking 使用
570
+ # 另一套对象结构,向兼容网关硬塞该字段会直接得到 400。
571
+ if effort in ("low", "medium", "high") and proto == "openai":
572
+ body["reasoning_effort"] = effort
537
573
  status, data, err = _post_interruptible(url, headers, body,
538
574
  allow_private, timeout,
539
575
  cancel_event)
package/app/core/jobs.py CHANGED
@@ -86,6 +86,17 @@ def configure(max_workers):
86
86
  return _target
87
87
 
88
88
 
89
+ def capacity_status():
90
+ """返回当前并发槽状态,供会产生管理记录的入口做无副作用预检。"""
91
+ with _pool_lock:
92
+ return {
93
+ "accepting": not _restart_drain and _alive < _target,
94
+ "active": _alive,
95
+ "limit": _target,
96
+ "restarting": _restart_drain,
97
+ }
98
+
99
+
89
100
  def start_worker():
90
101
  """加载并发保护配置;线程只在真实任务到达时创建。"""
91
102
  global _started
@@ -2016,6 +2016,12 @@ def bind_agent(agent, difficulty="default", task_type="", role=""):
2016
2016
  a = dict(agent)
2017
2017
  a["binding_configured"] = configured
2018
2018
  a["binding_mode"] = binding_mode
2019
+ # 统一思考档位:支持该能力的 CLI(当前 Codex)会把它下发为原生参数;
2020
+ # 其他 CLI 仍通过 difficulty 的模型路由获得相同的快/标准/深思语义。
2021
+ explicit_thinking = str(agent.get("_thinking") or "").strip().lower()
2022
+ a["reasoning_effort"] = ({"low": "low", "standard": "medium", "high": "high"}
2023
+ .get(explicit_thinking)
2024
+ or {"easy": "low", "hard": "high"}.get(difficulty, "medium"))
2019
2025
  if not r:
2020
2026
  return a
2021
2027
  merged = dict(agent.get("env") or {})
@@ -2997,16 +3003,17 @@ def resolve_orchestrator():
2997
3003
  return prov, model
2998
3004
 
2999
3005
 
3000
- 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=""):
3001
3007
  """§07 T2.2:精确匹配响应缓存的落盘路径(只缓存 ok 的幂等调用)。"""
3002
3008
  import hashlib as _h
3003
- 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)])
3004
3011
  name = _h.sha256(key.encode("utf-8")).hexdigest()[:24]
3005
3012
  return paths.DATA_DIR / "chat_cache" / (name + ".json")
3006
3013
 
3007
3014
 
3008
3015
  def chat(provider_id, model_name, prompt, max_tokens=2048, timeout=120, cache_ttl=0,
3009
- on_delta=None):
3016
+ on_delta=None, reasoning_effort=""):
3010
3017
  """直连供应商 API 做一次对话(编排者规划 / 连通性测试)。
3011
3018
 
3012
3019
  支持 anthropic / openai / google 三种协议;复用 SSRF 防护。
@@ -3020,7 +3027,8 @@ def chat(provider_id, model_name, prompt, max_tokens=2048, timeout=120, cache_tt
3020
3027
  """
3021
3028
  if cache_ttl > 0:
3022
3029
  try:
3023
- 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)
3024
3032
  if cache_path.is_file():
3025
3033
  age = time.time() - cache_path.stat().st_mtime
3026
3034
  if age <= cache_ttl:
@@ -3067,6 +3075,9 @@ def chat(provider_id, model_name, prompt, max_tokens=2048, timeout=120, cache_tt
3067
3075
  headers = {"Authorization": "Bearer " + use_key}
3068
3076
  body = {"model": model_name, "max_tokens": max_tokens,
3069
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
3070
3081
  return url, headers, body
3071
3082
 
3072
3083
  last_err = ""
@@ -3150,7 +3161,8 @@ def chat(provider_id, model_name, prompt, max_tokens=2048, timeout=120, cache_tt
3150
3161
  # §07 T2.2:cache_ttl>0 时落盘缓存(仅 ok 结果,原子写)
3151
3162
  if cache_ttl > 0:
3152
3163
  try:
3153
- 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)
3154
3166
  cache_path.parent.mkdir(parents=True, exist_ok=True)
3155
3167
  tmp = cache_path.with_suffix(".tmp")
3156
3168
  import json as _json
@@ -928,6 +928,10 @@ def _run_code(run, task, agents, ev, stats, mode):
928
928
  # ---- 难度(影响模型选择):用户指定 > 启发式 > 规划器判定
929
929
  difficulty = task.get("difficulty") or "auto"
930
930
  explicit = difficulty in ("easy", "hard")
931
+ if mode == "fast":
932
+ difficulty, explicit = "easy", True
933
+ elif mode == "expert":
934
+ difficulty, explicit = "hard", True
931
935
  if not explicit:
932
936
  difficulty = modelhub.classify_difficulty(task["goal"], task.get("verify_command"))
933
937
 
@@ -952,9 +956,10 @@ def _run_code(run, task, agents, ev, stats, mode):
952
956
 
953
957
  # ---- 规划。低风险短任务直接把目标作为单步计划,省掉一次独立模型调用;
954
958
  # 附件或较长的用户背景仍先规划,避免快速路径漏读约束。
955
- fast_path = (mode == "auto" and difficulty == "easy"
956
- and not task.get("attachments")
957
- and user_context_len < 600)
959
+ fast_path = (mode == "fast" or
960
+ (mode == "auto" and difficulty == "easy"
961
+ and not task.get("attachments")
962
+ and user_context_len < 600))
958
963
  planned = False
959
964
  if mode == "auto" and not fast_path:
960
965
  _wait_gate(run_id, ev)
@@ -1230,7 +1235,8 @@ def _run_code(run, task, agents, ev, stats, mode):
1230
1235
  "# 代码任务报告:%s" % task["title"], "",
1231
1236
  "- 结论:**%s**" % ("✅ 通过" if overall_pass else "❌ 未通过"),
1232
1237
  "- 编排模式:%s 实现者:%s 评审者:%s" % (
1233
- "智能" if mode == "auto" else "手动", impl.get("label"),
1238
+ {"auto": "自动", "fast": "快速", "expert": "专家",
1239
+ "manual": "手动"}.get(mode, mode), impl.get("label"),
1234
1240
  reviewer.get("label") if reviewer else "按验证结果省略"),
1235
1241
  "- 动态步骤:规划 %s;实现 %d 步;验证 %s;评审 %d 人;最多修复 %d 轮;换将 %s" % (
1236
1242
  "独立执行" if workflow["planning"] == "llm" else "并入实现",
@@ -1427,12 +1433,27 @@ def _run_direct(run, task, agents, ev, stats, mode):
1427
1433
  route = {}
1428
1434
  resume_ctx = _valid_resume(task, agents)
1429
1435
  bi = None
1430
- if resume_ctx is None and not (mode == "manual" and task.get("implementer")):
1436
+ direct_provider = (task.get("direct_provider_id") or "").strip()
1437
+ direct_model = (task.get("direct_model") or "").strip()
1438
+ direct_difficulty = ("hard" if task.get("thinking") == "high" else
1439
+ "easy" if task.get("thinking") == "low" else "default")
1440
+ if resume_ctx is None and direct_provider:
1441
+ bi = builtin_agent.resolve(direct_provider, direct_model, direct_difficulty)
1442
+ if bi is None:
1443
+ store.update_run(run_id, expected_status="running", status="failed",
1444
+ error="指定的对话厂商或模型不可用,请改用自动推荐或检查配置",
1445
+ ended_at=_now())
1446
+ return
1447
+ elif resume_ctx is None and not (mode == "manual" and task.get("implementer")):
1431
1448
  try:
1432
- bi = builtin_agent.resolve()
1449
+ bi = builtin_agent.resolve(difficulty=("hard" if task.get("thinking") == "high"
1450
+ else "easy" if task.get("thinking") == "low"
1451
+ else "default"))
1433
1452
  except Exception:
1434
1453
  bi = None
1435
1454
  if bi is not None:
1455
+ bi["reasoning_effort"] = {"low": "low", "standard": "medium", "high": "high"}.get(
1456
+ task.get("thinking"), "medium")
1436
1457
  impl = None
1437
1458
  route["implementer"] = "CodeBee(%s · %s)" % (bi["provider_name"], bi["model"])
1438
1459
  elif resume_ctx is not None:
@@ -2921,7 +2942,8 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
2921
2942
  "通过" if chapters_pass else "未通过",
2922
2943
  "通过" if global_pass else "未通过"),
2923
2944
  "- 编排模式:%s 作者:%s 评审组:%s" % (
2924
- "智能" if mode == "auto" else "手动",
2945
+ {"auto": "自动", "fast": "快速", "expert": "专家",
2946
+ "manual": "手动"}.get(mode, mode),
2925
2947
  impl.get("label"), "、".join(a.get("label") for a in critics)),
2926
2948
  "", "## 各章得分(章阈值 %.1f)" % threshold_ch, "",
2927
2949
  "| 章 | 标题 | " + " | ".join(dims) + " | 均分 | 达标 | 轮次 | 字数 |",
@@ -3077,6 +3099,11 @@ def _run_content_review(run, task, agents, ev, stats, mode):
3077
3099
  step_wd = _resume_workdir(resume_ctx, workdir) if resume_ctx else workdir
3078
3100
  difficulty = task.get("difficulty") or (
3079
3101
  "hard" if threshold >= 8.5 else "easy" if threshold <= 6 else "default")
3102
+ if mode == "fast":
3103
+ difficulty = "easy"
3104
+ elif mode == "expert":
3105
+ difficulty = "hard"
3106
+ workflow = task_compile.content_workflow(task, difficulty, mode=mode)
3080
3107
 
3081
3108
  # ---- 路由
3082
3109
  if resume_ctx is not None:
@@ -3100,6 +3127,10 @@ def _run_content_review(run, task, agents, ev, stats, mode):
3100
3127
  else:
3101
3128
  critics = _pick_critics_manual(agents, task)
3102
3129
 
3130
+ critic_pool = list(critics)
3131
+ if mode != "manual":
3132
+ critics = critic_pool[:workflow["reviewers"]]
3133
+
3103
3134
  _record_actual_route(run_id, task, agents, stats, impl, critics=critics,
3104
3135
  implement_reason=route.get("author", ""),
3105
3136
  review_reason=route.get("critics", ""))
@@ -3107,7 +3138,8 @@ def _run_content_review(run, task, agents, ev, stats, mode):
3107
3138
  # ---- 规划(小说为模板计划)
3108
3139
  _wait_gate(run_id, ev)
3109
3140
  plan = planner.make_novel_plan(_steered_task(run_id, task), impl, critics)
3110
- store.update_run(run_id, plan=plan, route=route, difficulty=difficulty)
3141
+ store.update_run(run_id, plan=plan, route=route, difficulty=difficulty,
3142
+ workflow=workflow)
3111
3143
 
3112
3144
  ms_path = os.path.join(workdir, ms_name)
3113
3145
 
@@ -3125,9 +3157,17 @@ def _run_content_review(run, task, agents, ev, stats, mode):
3125
3157
 
3126
3158
  # 编排者大纲:只对真实执行有意义;失败静默退回无大纲(喂入带指令的任务副本)
3127
3159
  outline = (planner.make_review_outline(_steered_task(run_id, task))
3128
- if impl.get("mode") != "mock" else None)
3160
+ if workflow["outline"] and impl.get("mode") != "mock" else None)
3129
3161
  if outline:
3130
- store.update_run(run_id, outline=outline)
3162
+ workflow = task_compile.content_workflow(
3163
+ task, difficulty, plan=outline, mode=mode)
3164
+ if mode != "manual":
3165
+ critics = critic_pool[:workflow["reviewers"]]
3166
+ store.update_run(run_id, outline=outline, workflow=workflow)
3167
+ _record_actual_route(run_id, task, agents, stats, impl, critics=critics,
3168
+ implement_reason=route.get("author", ""),
3169
+ review_reason=route.get("critics", ""))
3170
+ rounds = workflow["review_rounds"]
3131
3171
 
3132
3172
  # 1) 起草
3133
3173
  bestof_improvements = "" # 赛马败者精华(真实路径由选择器填充;mock 路径恒空)
@@ -3276,6 +3316,7 @@ def _run_content_review(run, task, agents, ev, stats, mode):
3276
3316
  "overall": overall, "mode": mode,
3277
3317
  "threshold": threshold, "rounds_used": history_rounds[-1]["round"] if history_rounds else 0,
3278
3318
  "scores": final_means, "history": history_rounds, "route": route,
3319
+ "workflow": workflow,
3279
3320
  }
3280
3321
 
3281
3322
  # 3) 报告
@@ -3285,8 +3326,12 @@ def _run_content_review(run, task, agents, ev, stats, mode):
3285
3326
  % ("✅ 达到发布标准" if publishable else "❌ 未达标,建议再修",
3286
3327
  overall, threshold, verdict["rounds_used"]),
3287
3328
  "- 编排模式:%s 起草/修订:%s 评审组:%s" % (
3288
- "智能" if mode == "auto" else "手动",
3289
- impl.get("label"), "、".join(a.get("label") for a in critics))]
3329
+ {"auto": "自动", "fast": "快速", "expert": "专家",
3330
+ "manual": "手动"}.get(mode, mode),
3331
+ impl.get("label"), "、".join(a.get("label") for a in critics)),
3332
+ "- 动态步骤:大纲 %s;评审 %d 人;最多 %d 轮(%s)" % (
3333
+ "启用" if workflow["outline"] else "省略",
3334
+ len(critics), rounds, workflow["reason"])]
3290
3335
  if route:
3291
3336
  lines.append("")
3292
3337
  lines.append("## 路由依据")
@@ -3637,6 +3682,7 @@ def execute_run(run_id):
3637
3682
  for _agent in agents:
3638
3683
  if isinstance(_agent, dict):
3639
3684
  _agent["_dispatch_task_type"] = task.get("type") or "direct"
3685
+ _agent["_thinking"] = task.get("thinking") or "auto"
3640
3686
  store.update_run(run_id, route_plan={
3641
3687
  "task": task_spec,
3642
3688
  "implement": router.route_plan(agents, "implement", task_spec, stats),
@@ -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()
@@ -58,13 +58,52 @@ def find_git_bash():
58
58
  return None
59
59
 
60
60
 
61
- def resolve_command(command):
61
+ def resolve_command(command):
62
62
  """命令名 → 可直接 spawn 的 argv 前缀(.cmd/.bat 垫片必须经 cmd /c)。"""
63
63
  path = shutil.which(command) or command
64
64
  low = path.lower()
65
65
  if low.endswith(".cmd") or low.endswith(".bat"):
66
66
  return ["cmd", "/c", path]
67
- return [path]
67
+ return [path]
68
+
69
+
70
+ def _git_repo_issue(workdir):
71
+ """返回工作目录 Git 仓库的可读性问题;无 Git 仓库时返回空串。
72
+
73
+ Aider 会在启动后自行读取索引。索引包缺失时它通常先写入 .gitignore,
74
+ 再在深层调用中才报错,用户会误以为任务卡住;提前做一次廉价预检能把
75
+ 错误变成可操作的任务失败信息。
76
+ """
77
+ if not workdir or not os.path.isdir(workdir):
78
+ return ""
79
+ git_dir = os.path.join(workdir, ".git")
80
+ if not os.path.exists(git_dir):
81
+ return ""
82
+ try:
83
+ probe = subprocess.run(
84
+ ["git", "-C", workdir, "rev-parse", "--git-dir"],
85
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE,
86
+ text=True, timeout=5, check=False,
87
+ )
88
+ except (OSError, subprocess.SubprocessError):
89
+ return "Git 命令不可用,无法读取仓库;请安装 Git 或改用非 Aider 实现器"
90
+ if probe.returncode != 0:
91
+ detail = (probe.stderr or probe.stdout or "仓库元数据不可读").strip()
92
+ return "Git 仓库不可读:%s;请先执行 git fsck/恢复 .git 对象后重试" % detail[-300:]
93
+ try:
94
+ probe = subprocess.run(
95
+ ["git", "-C", workdir, "status", "--porcelain", "--untracked-files=no"],
96
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE,
97
+ text=True, timeout=8, check=False,
98
+ )
99
+ except subprocess.TimeoutExpired:
100
+ return "Git 仓库检查超时;请先执行 git status/git fsck 修复后重试"
101
+ except OSError:
102
+ return "Git 命令不可用,无法读取仓库;请安装 Git 或改用非 Aider 实现器"
103
+ if probe.returncode != 0:
104
+ detail = (probe.stderr or probe.stdout or "仓库对象或索引不可读").strip()
105
+ return "Git 仓库损坏或对象缺失:%s;请先执行 git fsck 并恢复仓库后重试" % detail[-300:]
106
+ return ""
68
107
 
69
108
 
70
109
  def _npm_shim_bypass(argv):
@@ -844,8 +883,9 @@ def _build_call(agent, kind, sid, readonly, model, prompt, images=None, workdir=
844
883
  stdin_text = None
845
884
  tmp_files = []
846
885
  imgs = [str(p) for p in (images or []) if p]
847
- if kind == "codex":
848
- cp = agent.get("codex_provider")
886
+ if kind == "codex":
887
+ cp = agent.get("codex_provider")
888
+ reasoning = str(agent.get("reasoning_effort") or "").strip().lower()
849
889
  if sid:
850
890
  # resume 子命令不支持 -s 也不支持 --full-auto(0.154 实测:
851
891
  # "unexpected argument '--full-auto'")——读/写模式都用 -c sandbox_mode
@@ -855,16 +895,18 @@ def _build_call(agent, kind, sid, readonly, model, prompt, images=None, workdir=
855
895
  if model:
856
896
  argv += ["-m", model]
857
897
  argv += ["-c", 'sandbox_mode="%s"' % _codex_sandbox(readonly)]
858
- if cp:
859
- argv += _codex_provider_args(cp)
898
+ if cp:
899
+ argv += _codex_provider_args(cp)
860
900
  else:
861
901
  argv = resolve_command(agent["command"]) + [
862
902
  "exec", "--skip-git-repo-check", "--json",
863
903
  "-s", _codex_sandbox(readonly)]
864
904
  if model:
865
905
  argv += ["-m", model]
866
- if cp:
867
- argv += _codex_provider_args(cp)
906
+ if cp:
907
+ argv += _codex_provider_args(cp)
908
+ if reasoning in ("low", "medium", "high", "xhigh"):
909
+ argv += ["-c", 'model_reasoning_effort="%s"' % reasoning]
868
910
  # codex exec 与 exec resume 都支持 -i:图片直接附到 prompt(exec resume 的
869
911
  # -i 附在恢复后发送的首条消息上,即本次 stdin prompt)
870
912
  for p in imgs:
@@ -912,10 +954,11 @@ def _build_call(agent, kind, sid, readonly, model, prompt, images=None, workdir=
912
954
  if model:
913
955
  argv += ["-m", model]
914
956
  stdin_text = prompt
915
- elif kind == "aider":
916
- argv = resolve_command(agent["command"]) + [
917
- "--yes-always", "--no-auto-commits", "--no-check-update",
918
- # 网关自定义模型名(glm-5.1 等)litellm 全都不认识,警告页+建议列表
957
+ elif kind == "aider":
958
+ argv = resolve_command(agent["command"]) + [
959
+ "--yes-always", "--no-auto-commits", "--no-check-update",
960
+ "--no-gitignore", "--no-pretty", "--no-stream",
961
+ # 网关自定义模型名(glm-5.1 等)litellm 全都不认识,警告页+建议列表
919
962
  # 纯属刷屏(2026-09-20 实测占满步骤日志头部)
920
963
  "--no-show-model-warnings", "--message", prompt]
921
964
  if model:
@@ -987,7 +1030,7 @@ def _stall_timeout(env_name, default):
987
1030
  return default
988
1031
 
989
1032
 
990
- def run_agent(agent, prompt, workdir=None, readonly=True,
1033
+ def run_agent(agent, prompt, workdir=None, readonly=True,
991
1034
  timeout=DEFAULT_TIMEOUT, cancel_event=None, log_path=None, resume=None,
992
1035
  images=None, require_tools=False):
993
1036
  """执行一次智能体调用,返回统一结构
@@ -1010,7 +1053,14 @@ def run_agent(agent, prompt, workdir=None, readonly=True,
1010
1053
  orch_timeout_ms = (agent.get("orch") or {}).get("timeout_ms")
1011
1054
  if orch_timeout_ms:
1012
1055
  timeout = float(orch_timeout_ms) / 1000.0
1013
- kind = agent.get("kind", "generic")
1056
+ kind = agent.get("kind", "generic")
1057
+ if kind == "aider":
1058
+ repo_issue = _git_repo_issue(workdir)
1059
+ if repo_issue:
1060
+ return {"ok": False, "text": "", "json": None, "cost_usd": 0.0,
1061
+ "tokens": 0, "usage": None, "error": repo_issue,
1062
+ "error_code": ErrorCode.VENDOR_ERROR, "raw": None,
1063
+ "kind": kind, "model": agent.get("model")}
1014
1064
  if kind == "codex":
1015
1065
  stall_t = _stall_timeout("TUTTI_CODEX_STALL_TIMEOUT", 600)
1016
1066
  elif kind == "claude":
@@ -176,10 +176,17 @@ def apply_upgrade(port=None):
176
176
  _PENDING_PORT = int(port) if port else None
177
177
  except (TypeError, ValueError):
178
178
  _PENDING_PORT = None
179
- if install_mode() != "npm":
180
- return {"error": "当前安装方式不支持自动升级(见版本页说明)"}
181
- from . import store, jobs
182
- try:
179
+ if install_mode() != "npm":
180
+ return {"error": "当前安装方式不支持自动升级(见版本页说明)"}
181
+ from . import store, jobs
182
+ capacity = jobs.capacity_status()
183
+ if not capacity["accepting"]:
184
+ if capacity["restarting"]:
185
+ return {"error": "服务正在完成升级重启,请稍后再试"}
186
+ return {"error": "当前有 %d 个任务运行,已达并发保护上限(%d);"
187
+ "升级未启动且不会排队,请在任务结束后重试"
188
+ % (capacity["active"], capacity["limit"])}
189
+ try:
183
190
  run = store.create_run(
184
191
  "mgmt", "升级 CodeBee 本体(npm install -g %s@latest)" % _PKG_NAME,
185
192
  entry_id="__self__", op="selfupgrade")
@@ -190,10 +197,12 @@ def apply_upgrade(port=None):
190
197
  jobs.enqueue({"kind": "selfupgrade", "run_id": run["id"]})
191
198
  except Exception as exc:
192
199
  # run 已持久化;启动失败时显式收口,版本页不能停在误导性的待启动状态。
193
- log.exception("selfupdate: 升级任务启动失败 run=%s", run["id"])
194
200
  if isinstance(exc, jobs.JobsBusyError):
195
201
  message = "升级未启动:%s" % str(exc)
202
+ log.warning("selfupdate: 升级任务因并发满载未启动 run=%s: %s",
203
+ run["id"], exc)
196
204
  else:
205
+ log.exception("selfupdate: 升级任务启动失败 run=%s", run["id"])
197
206
  # 仅并发保护类错误可直接展示;其他内部异常继续隐藏实现细节。
198
207
  message = "升级任务启动失败,本次未排队,请稍后重试"
199
208
  try: