codebee 0.1.29 → 0.1.30

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,19 @@ README 元数据带回,供老版本在「发现新版本」时展示新版更
6
6
 
7
7
  ## 未发布
8
8
 
9
+ ## v0.1.30(2026-09-21)
10
+
11
+ ### ✨ 新功能
12
+
13
+ - 微信直连配置更省事:新增「扫描」按钮自动找出本机全部 Python(标注位数与是否已装依赖,点选即回填);选中未装依赖的 64 位解释器可「一键安装 wechatauto-replica」,不用再自己开 conda 敲命令。
14
+ - 桌面蜜蜂右键新增「群摘要」:直达群摘要面板(已打开的页面只聚焦不重复开)。
15
+ - 直连任务可选指定厂商与模型。
16
+
17
+ ### 🛠 问题修复
18
+
19
+ - 修复没装 Pillow 的电脑上蜜蜂永远显示手绘丑蜜蜂:现在用系统自带能力直接显示新形象贴图(普通用户升级即变好看,无需装任何额外依赖)。
20
+ - 禅道自动化四个开关与扫描间隔紧凑排成一行。
21
+
9
22
  ## v0.1.29(2026-09-21)
10
23
 
11
24
  ### ✨ 新功能
package/README.md CHANGED
@@ -23,11 +23,25 @@ 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
+ - 微信直连配置更省事:「扫描」自动找本机 Python(标注位数与依赖状态,点选即用),选中后可一键安装 wechatauto-replica。
27
+ - WeChat setup is now effortless: a Scan button finds local Pythons (with bitness and dependency badges), and one click installs wechatauto-replica.
28
+
29
+ - 桌面蜜蜂右键新增「群摘要」:直达群摘要面板,已打开的页面只聚焦不重复开。
30
+ - The desktop bee's right-click menu now has "Group Digests" — it focuses the existing page instead of opening a duplicate.
31
+
32
+ - 没装 Pillow 的电脑上蜜蜂也会显示新形象贴图了(修复手绘丑蜜蜂问题)。
33
+ - Bees now show the new artwork even without Pillow installed (fixes the ugly hand-drawn fallback).
28
34
 
29
35
  ### 🛠 问题修复 · Fixes
30
36
 
37
+ - 禅道自动化开关与扫描间隔紧凑排成一行。
38
+ - ZenTao automation toggles and the scan interval now fit on one tidy row.
39
+
40
+ ### 上一版亮点 · Previous release
41
+
42
+ - 升级后蜜蜂自动换新装:服务重启后旧蜜蜂自动让位,新蜜蜂带新形象上线。
43
+ - After upgrading, the desktop bee swaps itself for the new look: the old bee steps aside when the service restarts.
44
+
31
45
  - 去掉蜜蜂底部的任务进度条,窗口更干净。
32
46
  - Removed the task progress bar under the desktop bee for a cleaner look.
33
47
 
@@ -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,9 @@ 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
+ if effort in ("low", "medium", "high") and proto != "google":
570
+ body["reasoning_effort"] = effort
537
571
  status, data, err = _post_interruptible(url, headers, body,
538
572
  allow_private, timeout,
539
573
  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 {})
@@ -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)
@@ -1427,12 +1432,27 @@ def _run_direct(run, task, agents, ev, stats, mode):
1427
1432
  route = {}
1428
1433
  resume_ctx = _valid_resume(task, agents)
1429
1434
  bi = None
1430
- if resume_ctx is None and not (mode == "manual" and task.get("implementer")):
1435
+ direct_provider = (task.get("direct_provider_id") or "").strip()
1436
+ direct_model = (task.get("direct_model") or "").strip()
1437
+ direct_difficulty = ("hard" if task.get("thinking") == "high" else
1438
+ "easy" if task.get("thinking") == "low" else "default")
1439
+ if resume_ctx is None and direct_provider:
1440
+ bi = builtin_agent.resolve(direct_provider, direct_model, direct_difficulty)
1441
+ if bi is None:
1442
+ store.update_run(run_id, expected_status="running", status="failed",
1443
+ error="指定的对话厂商或模型不可用,请改用自动推荐或检查配置",
1444
+ ended_at=_now())
1445
+ return
1446
+ elif resume_ctx is None and not (mode == "manual" and task.get("implementer")):
1431
1447
  try:
1432
- bi = builtin_agent.resolve()
1448
+ bi = builtin_agent.resolve(difficulty=("hard" if task.get("thinking") == "high"
1449
+ else "easy" if task.get("thinking") == "low"
1450
+ else "default"))
1433
1451
  except Exception:
1434
1452
  bi = None
1435
1453
  if bi is not None:
1454
+ bi["reasoning_effort"] = {"low": "low", "standard": "medium", "high": "high"}.get(
1455
+ task.get("thinking"), "medium")
1436
1456
  impl = None
1437
1457
  route["implementer"] = "CodeBee(%s · %s)" % (bi["provider_name"], bi["model"])
1438
1458
  elif resume_ctx is not None:
@@ -3077,6 +3097,11 @@ def _run_content_review(run, task, agents, ev, stats, mode):
3077
3097
  step_wd = _resume_workdir(resume_ctx, workdir) if resume_ctx else workdir
3078
3098
  difficulty = task.get("difficulty") or (
3079
3099
  "hard" if threshold >= 8.5 else "easy" if threshold <= 6 else "default")
3100
+ if mode == "fast":
3101
+ difficulty = "easy"
3102
+ elif mode == "expert":
3103
+ difficulty = "hard"
3104
+ workflow = task_compile.content_workflow(task, difficulty, mode=mode)
3080
3105
 
3081
3106
  # ---- 路由
3082
3107
  if resume_ctx is not None:
@@ -3100,6 +3125,10 @@ def _run_content_review(run, task, agents, ev, stats, mode):
3100
3125
  else:
3101
3126
  critics = _pick_critics_manual(agents, task)
3102
3127
 
3128
+ critic_pool = list(critics)
3129
+ if mode != "manual":
3130
+ critics = critic_pool[:workflow["reviewers"]]
3131
+
3103
3132
  _record_actual_route(run_id, task, agents, stats, impl, critics=critics,
3104
3133
  implement_reason=route.get("author", ""),
3105
3134
  review_reason=route.get("critics", ""))
@@ -3107,7 +3136,8 @@ def _run_content_review(run, task, agents, ev, stats, mode):
3107
3136
  # ---- 规划(小说为模板计划)
3108
3137
  _wait_gate(run_id, ev)
3109
3138
  plan = planner.make_novel_plan(_steered_task(run_id, task), impl, critics)
3110
- store.update_run(run_id, plan=plan, route=route, difficulty=difficulty)
3139
+ store.update_run(run_id, plan=plan, route=route, difficulty=difficulty,
3140
+ workflow=workflow)
3111
3141
 
3112
3142
  ms_path = os.path.join(workdir, ms_name)
3113
3143
 
@@ -3125,9 +3155,17 @@ def _run_content_review(run, task, agents, ev, stats, mode):
3125
3155
 
3126
3156
  # 编排者大纲:只对真实执行有意义;失败静默退回无大纲(喂入带指令的任务副本)
3127
3157
  outline = (planner.make_review_outline(_steered_task(run_id, task))
3128
- if impl.get("mode") != "mock" else None)
3158
+ if workflow["outline"] and impl.get("mode") != "mock" else None)
3129
3159
  if outline:
3130
- store.update_run(run_id, outline=outline)
3160
+ workflow = task_compile.content_workflow(
3161
+ task, difficulty, plan=outline, mode=mode)
3162
+ if mode != "manual":
3163
+ critics = critic_pool[:workflow["reviewers"]]
3164
+ store.update_run(run_id, outline=outline, workflow=workflow)
3165
+ _record_actual_route(run_id, task, agents, stats, impl, critics=critics,
3166
+ implement_reason=route.get("author", ""),
3167
+ review_reason=route.get("critics", ""))
3168
+ rounds = workflow["review_rounds"]
3131
3169
 
3132
3170
  # 1) 起草
3133
3171
  bestof_improvements = "" # 赛马败者精华(真实路径由选择器填充;mock 路径恒空)
@@ -3276,6 +3314,7 @@ def _run_content_review(run, task, agents, ev, stats, mode):
3276
3314
  "overall": overall, "mode": mode,
3277
3315
  "threshold": threshold, "rounds_used": history_rounds[-1]["round"] if history_rounds else 0,
3278
3316
  "scores": final_means, "history": history_rounds, "route": route,
3317
+ "workflow": workflow,
3279
3318
  }
3280
3319
 
3281
3320
  # 3) 报告
@@ -3285,8 +3324,12 @@ def _run_content_review(run, task, agents, ev, stats, mode):
3285
3324
  % ("✅ 达到发布标准" if publishable else "❌ 未达标,建议再修",
3286
3325
  overall, threshold, verdict["rounds_used"]),
3287
3326
  "- 编排模式:%s 起草/修订:%s 评审组:%s" % (
3288
- "智能" if mode == "auto" else "手动",
3289
- impl.get("label"), "、".join(a.get("label") for a in critics))]
3327
+ {"auto": "自动", "fast": "快速", "expert": "专家",
3328
+ "manual": "手动"}.get(mode, mode),
3329
+ impl.get("label"), "、".join(a.get("label") for a in critics)),
3330
+ "- 动态步骤:大纲 %s;评审 %d 人;最多 %d 轮(%s)" % (
3331
+ "启用" if workflow["outline"] else "省略",
3332
+ len(critics), rounds, workflow["reason"])]
3290
3333
  if route:
3291
3334
  lines.append("")
3292
3335
  lines.append("## 路由依据")
@@ -3637,6 +3680,7 @@ def execute_run(run_id):
3637
3680
  for _agent in agents:
3638
3681
  if isinstance(_agent, dict):
3639
3682
  _agent["_dispatch_task_type"] = task.get("type") or "direct"
3683
+ _agent["_thinking"] = task.get("thinking") or "auto"
3640
3684
  store.update_run(run_id, route_plan={
3641
3685
  "task": task_spec,
3642
3686
  "implement": router.route_plan(agents, "implement", task_spec, stats),
@@ -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:
package/app/core/store.py CHANGED
@@ -104,12 +104,12 @@ def create_task(payload):
104
104
  if not wd.is_dir():
105
105
  raise ValueError("工作目录不存在: %s" % workdir)
106
106
  mode = payload.get("mode")
107
- if mode not in ("auto", "manual"):
108
- mode = "manual" if payload.get("implementer") else "auto"
107
+ if mode not in ("auto", "fast", "expert", "manual"):
108
+ mode = "manual" if payload.get("implementer") else "auto"
109
109
  difficulty = payload.get("difficulty")
110
110
  if difficulty not in ("auto", "easy", "hard", "default"):
111
111
  difficulty = "auto"
112
- task = {
112
+ task = {
113
113
  "id": _new_id("t"), "type": flow["id"], "engine": flow["engine"],
114
114
  "title": title, "goal": goal,
115
115
  "context": _text(payload.get("context"), "context"),
@@ -119,8 +119,16 @@ def create_task(payload):
119
119
  "implementer": payload.get("implementer") or "",
120
120
  "attachments": [],
121
121
  "created_at": time.strftime("%Y-%m-%d %H:%M:%S"),
122
- "status": "created",
123
- }
122
+ "status": "created",
123
+ }
124
+ thinking = str(payload.get("thinking") or "standard").strip().lower()
125
+ task["thinking"] = thinking if thinking in ("auto", "low", "standard", "high") else "auto"
126
+ if flow["engine"] == "direct":
127
+ task["direct_provider_id"] = _text(payload.get("direct_provider_id"),
128
+ "direct_provider_id")[:80]
129
+ task["direct_model"] = _text(payload.get("direct_model"), "direct_model")[:160]
130
+ if task["direct_model"] and not task["direct_provider_id"]:
131
+ raise ValueError("指定对话模型时必须同时指定厂商")
124
132
  # 代码版本:仅当引用合法才固化(流水线执行前据此检出任务分支)
125
133
  from . import gitmod
126
134
  git_rev = _text(payload.get("git_rev"), "git_rev")
@@ -133,8 +141,8 @@ def create_task(payload):
133
141
  task["git_rev"] = git_rev
134
142
  if flow["engine"] == "code":
135
143
  task["verify_command"] = _text(payload.get("verify_command"), "verify_command")
136
- elif flow["engine"] == "direct":
137
- pass # 直连任务:无验证命令也无评审参数,目标+附件即全部输入
144
+ elif flow["engine"] == "direct":
145
+ pass # 直连任务:无验证命令也无评审参数,目标+附件即全部输入
138
146
  else:
139
147
  ms = _text(payload.get("manuscript") or flow.get("manuscript") or "manuscript.md",
140
148
  "manuscript")
@@ -478,7 +486,7 @@ def load_all():
478
486
 
479
487
  # ---------------------------------------------------------------- 运行(含管理操作)
480
488
 
481
- def create_run(kind, title, task_id=None, entry_id=None, op=None):
489
+ def create_run(kind, title, task_id=None, entry_id=None, op=None):
482
490
  run = {
483
491
  "id": _new_id("r" if kind == "orchestration" else "m"),
484
492
  "kind": kind, # orchestration | mgmt
@@ -488,8 +496,21 @@ def create_run(kind, title, task_id=None, entry_id=None, op=None):
488
496
  "created_at": time.strftime("%Y-%m-%d %H:%M:%S"),
489
497
  "started_at": None, "ended_at": None,
490
498
  "cost_usd": 0.0, "tokens": 0, "error": "",
491
- "verdict": None, "summary": "",
492
- }
499
+ "verdict": None, "summary": "",
500
+ }
501
+ if kind == "orchestration" and task_id:
502
+ task = get_task(task_id) or {}
503
+ try:
504
+ from . import usage
505
+ eta = usage.estimate(
506
+ task_type=task.get("type") or "", mode=task.get("mode") or "auto",
507
+ thinking=task.get("thinking") or "standard",
508
+ rounds=task.get("rounds"))
509
+ run["estimated_duration_s"] = eta.get("estimated_duration_s")
510
+ run["estimated_p90_s"] = eta.get("p90_duration_s")
511
+ run["estimate_source"] = eta.get("duration_source")
512
+ except Exception:
513
+ pass
493
514
  rdir = paths.RUNS_DIR / run["id"]
494
515
  (rdir / "steps").mkdir(parents=True, exist_ok=True)
495
516
  with LOCK:
@@ -814,12 +835,36 @@ def run_workdir(run_id):
814
835
  _SKIP_DIRS = {".git", "node_modules", "__pycache__", ".venv", "venv", ".idea", ".vscode"}
815
836
 
816
837
  # 构建产物/依赖缓存目录:里面的文件是工具链再生成的,不是任务成品
817
- _BUILD_DIRS = {
838
+ _BUILD_DIRS = {
818
839
  "target", "build", "dist", "out", "bin", "obj", # 通用构建输出
819
840
  "surefire", "failsafe-reports", "test-output", "reports", # 测试/报告输出
820
841
  ".next", ".nuxt", ".output", ".gradle", ".gradle-home", # 前端/Gradle
821
842
  "__MACOSX",
822
- }
843
+ }
844
+
845
+ # CLI 自身的历史/评审中间件不属于用户成果。它们通常落在工作目录根部,
846
+ # 只按隐藏目录过滤会漏掉 Aider 的 .aider.* 与流程生成的 *_review.json。
847
+ _PROCESS_ARTIFACT_NAMES = {
848
+ ".aider.chat.history.md", ".aider.input.history", ".aider.input.history.md",
849
+ ".aider.tags.cache.v4", ".aider.tags.cache.v3", ".aider.conf.yml",
850
+ }
851
+ _PROCESS_ARTIFACT_SUFFIXES = ("_review.json", ".review.json")
852
+ _PROCESS_ARTIFACT_PREFIXES = ("tutti_prompt_", "_tutti_prompt_")
853
+
854
+
855
+ def _is_process_artifact(rel_name):
856
+ """判断工作目录文件是否为 CLI/评审过程产物,而非可交付成果。"""
857
+ name = str(rel_name or "").replace("\\", "/")
858
+ base = name.rsplit("/", 1)[-1].lower()
859
+ if base in _PROCESS_ARTIFACT_NAMES:
860
+ return True
861
+ if base.startswith(".") and base.startswith(".aider"):
862
+ return True
863
+ if base.endswith(_PROCESS_ARTIFACT_SUFFIXES):
864
+ return True
865
+ if base.startswith(_PROCESS_ARTIFACT_PREFIXES):
866
+ return True
867
+ return False
823
868
 
824
869
 
825
870
  def task_first_start(task_id, fallback=""):
@@ -880,7 +925,10 @@ def run_artifacts(run_id, limit=200):
880
925
  continue
881
926
  if st.st_mtime < t0:
882
927
  continue
883
- files.append({"name": str(p.relative_to(root)).replace("\\", "/"),
928
+ rel_name = str(p.relative_to(root)).replace("\\", "/")
929
+ if _is_process_artifact(rel_name):
930
+ continue
931
+ files.append({"name": rel_name,
884
932
  "size": st.st_size, "mtime": int(st.st_mtime)})
885
933
  if len(files) >= 800: # 防超大目录拖垮接口;截断后再排序取最新
886
934
  break
@@ -69,6 +69,9 @@ def compile_task(task):
69
69
  "dimension": dimension,
70
70
  "difficulty": difficulty,
71
71
  "difficulty_reason": difficulty_reason,
72
+ "thinking": (str(raw.get("thinking") or "standard").lower()
73
+ if str(raw.get("thinking") or "standard").lower()
74
+ in ("auto", "low", "standard", "high") else "auto"),
72
75
  "capabilities": _capabilities(raw, dimension, engine),
73
76
  "deliverable": deliverable,
74
77
  "quality_dimensions": rubric,
@@ -97,26 +100,37 @@ def code_workflow(task, difficulty, plan=None, mode="auto", planned=True):
97
100
  rec = (plan or {}).get("workflow") if isinstance(plan, Mapping) else None
98
101
  rec = rec if isinstance(rec, Mapping) else {}
99
102
  has_verify = bool(str(task.get("verify_command") or "").strip())
100
- easy_auto = mode == "auto" and difficulty == "easy"
103
+ risk_text = " ".join(str(task.get(k) or "") for k in ("title", "goal", "context")).lower()
104
+ high_risk = bool(any(k in risk_text for k in (
105
+ "安全", "权限", "鉴权", "认证", "加密", "密钥", "注入", "迁移", "数据库",
106
+ "并发", "竞态", "支付", "public api", "公共 api", "breaking change",
107
+ "security", "permission", "auth", "migration", "concurrency")))
108
+ fast_mode = mode == "fast"
109
+ expert_mode = mode == "expert"
110
+ easy_auto = (mode == "auto" and difficulty == "easy") or fast_mode
101
111
 
102
112
  review_required = True
103
113
  max_repairs = 2
104
114
  allow_switch = True
105
115
  reason = "复杂或手动任务使用完整质量门禁"
106
116
  if easy_auto:
107
- review_required = not has_verify
117
+ review_required = high_risk or not has_verify
108
118
  max_repairs = 1
109
119
  allow_switch = False
110
120
  reason = ("低风险任务以确定性验证代替模型评审"
111
121
  if has_verify else "低风险任务无验证命令,保留一次模型评审")
112
122
  if isinstance(rec.get("review_required"), bool):
113
- review_required = rec["review_required"] or not has_verify
123
+ review_required = high_risk or rec["review_required"] or not has_verify
114
124
  try:
115
125
  if "max_repair_rounds" in rec:
116
- max_repairs = max(0, min(1, int(rec["max_repair_rounds"])))
126
+ max_repairs = max(1 if not has_verify else 0,
127
+ min(1, int(rec["max_repair_rounds"])))
117
128
  except (TypeError, ValueError):
118
129
  pass
119
- elif mode == "auto" and difficulty == "hard":
130
+ elif expert_mode or (mode == "auto" and difficulty == "hard"):
131
+ review_required = True
132
+ max_repairs = 2
133
+ allow_switch = True
120
134
  try:
121
135
  if "max_repair_rounds" in rec:
122
136
  max_repairs = max(1, min(2, int(rec["max_repair_rounds"])))
@@ -148,6 +162,13 @@ def content_workflow(task, difficulty, plan=None, mode="auto"):
148
162
  light_types = {"email", "weekly_report", "translation"}
149
163
  deep_types = {"novel", "research", "tech_proposal"}
150
164
 
165
+ if mode == "fast":
166
+ return {"outline": False, "reviewers": 1, "review_rounds": 1,
167
+ "reason": "快速模式省略独立大纲,仅保留一次快速评审"}
168
+ if mode == "expert":
169
+ return {"outline": True, "reviewers": 2,
170
+ "review_rounds": min(requested_rounds, 3),
171
+ "reason": "专家模式启用完整大纲、多评审与修订门禁"}
151
172
  if mode != "auto":
152
173
  return {"outline": True, "reviewers": 0,
153
174
  "review_rounds": requested_rounds,