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 +24 -0
- package/README.md +24 -2
- package/app/core/builtin_agent.py +38 -2
- package/app/core/jobs.py +11 -0
- package/app/core/modelhub.py +17 -5
- package/app/core/pipeline.py +58 -12
- package/app/core/planner.py +34 -10
- package/app/core/repeat_guard.py +3 -3
- package/app/core/runner.py +64 -14
- package/app/core/selfupdate.py +14 -5
- package/app/core/store.py +61 -13
- package/app/core/task_compile.py +27 -5
- package/app/core/usage.py +55 -14
- package/app/core/wxdigest.py +140 -0
- package/app/main.py +22 -1
- package/app/pet.py +1564 -1535
- package/app/ui/app.js +249 -58
- package/app/ui/i18n.js +36 -0
- package/app/ui/index.html +1172 -1165
- package/app/ui/style.css +44 -0
- package/package.json +1 -1
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
|
-
-
|
|
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("
|
|
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
|
package/app/core/modelhub.py
CHANGED
|
@@ -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),
|
|
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
|
package/app/core/pipeline.py
CHANGED
|
@@ -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 == "
|
|
956
|
-
and
|
|
957
|
-
|
|
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
|
-
"
|
|
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
|
-
|
|
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
|
-
"
|
|
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
|
-
|
|
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
|
-
"
|
|
3289
|
-
|
|
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),
|
package/app/core/planner.py
CHANGED
|
@@ -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"]:
|
package/app/core/repeat_guard.py
CHANGED
|
@@ -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
|
-
|
|
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 = (
|
|
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/runner.py
CHANGED
|
@@ -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
|
-
|
|
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":
|
package/app/core/selfupdate.py
CHANGED
|
@@ -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
|
-
|
|
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:
|