codebee 0.1.28 → 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 +23 -0
- package/README.md +22 -0
- package/app/core/builtin_agent.py +36 -2
- package/app/core/dispatch.py +3 -1
- package/app/core/flows.py +1 -1
- package/app/core/jobs.py +23 -0
- package/app/core/modelhub.py +6 -0
- package/app/core/pipeline.py +118 -26
- package/app/core/planner.py +54 -17
- package/app/core/router.py +16 -9
- package/app/core/runner.py +64 -14
- package/app/core/selfupdate.py +26 -12
- package/app/core/store.py +61 -13
- package/app/core/task_compile.py +111 -0
- package/app/core/usage.py +55 -14
- package/app/core/wxdigest.py +140 -0
- package/app/main.py +28 -1
- package/app/pet.py +95 -58
- package/app/ui/app.js +209 -11
- package/app/ui/i18n.js +35 -0
- package/app/ui/index.html +28 -7
- package/app/ui/style.css +62 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,29 @@ 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
|
+
|
|
22
|
+
## v0.1.29(2026-09-21)
|
|
23
|
+
|
|
24
|
+
### ✨ 新功能
|
|
25
|
+
|
|
26
|
+
- 升级后蜜蜂自动换新装:修复「升级后还是旧的丑蜜蜂」——服务重启后旧蜜蜂会自动让位,由新服务拉起带新形象的新蜜蜂(此前旧蜜蜂占着位置永不退场)。
|
|
27
|
+
|
|
28
|
+
### 🛠 问题修复
|
|
29
|
+
|
|
30
|
+
- 去掉蜜蜂底部的任务进度条(真机截图反馈):窗口收紧贴着蜂体,画面更干净。
|
|
31
|
+
|
|
9
32
|
## v0.1.28(2026-09-21)
|
|
10
33
|
|
|
11
34
|
### 🛠 问题修复
|
package/README.md
CHANGED
|
@@ -21,8 +21,30 @@ Kimi Code、MiMo Code、Grok Build、Pi、DeepSeek Harness……),提供
|
|
|
21
21
|
不会把你的任务内容交给任何第三方。
|
|
22
22
|
|
|
23
23
|
<!-- relnotes:start -->
|
|
24
|
+
### ✨ 新功能 · New
|
|
25
|
+
|
|
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).
|
|
34
|
+
|
|
24
35
|
### 🛠 问题修复 · Fixes
|
|
25
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
|
+
|
|
45
|
+
- 去掉蜜蜂底部的任务进度条,窗口更干净。
|
|
46
|
+
- Removed the task progress bar under the desktop bee for a cleaner look.
|
|
47
|
+
|
|
26
48
|
- 「关于与更新」页的版本信息与更新说明改为左对齐阅读,不再整块居中。
|
|
27
49
|
- The version info and release notes on the About page are now left-aligned for easier reading.
|
|
28
50
|
|
|
@@ -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,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/dispatch.py
CHANGED
|
@@ -103,7 +103,9 @@ def _online_model_bonus(entry, task_type, role):
|
|
|
103
103
|
rate = float(metrics.get("success_rate") or 0.0)
|
|
104
104
|
success_score = max(-6.0, min(6.0, (rate - 0.75) * 18.0))
|
|
105
105
|
p95 = max(0.0, float(metrics.get("p95_duration_s") or 0.0))
|
|
106
|
-
|
|
106
|
+
# 模型链也必须让真实延迟影响排序,不能让 P95 数分钟的链靠静态质量分
|
|
107
|
+
# 长期压在十几秒的健康链前面。
|
|
108
|
+
latency_score = -min(12.0, max(0.0, (p95 - 30.0) / 8.0))
|
|
107
109
|
cost = max(0.0, float(metrics.get("avg_cost_usd") or 0.0))
|
|
108
110
|
cost_score = -min(3.0, max(0.0, (cost - 0.01) / 0.01))
|
|
109
111
|
total = round(success_score + latency_score + cost_score, 2)
|
package/app/core/flows.py
CHANGED
|
@@ -36,7 +36,7 @@ BUILTIN_FLOWS = [
|
|
|
36
36
|
"note": "CodeBee 直连模型 API 干活(无 CLI 进程),无可用供应商时回退本机 CLI;无拆解/评审(快)"},
|
|
37
37
|
{"id": "code", "name": "代码", "icon": "i-code", "engine": "code", "builtin": True,
|
|
38
38
|
"goal_hint": "要实现/修复什么(一句话)",
|
|
39
|
-
"note": "
|
|
39
|
+
"note": "按难度动态编排:简单任务走实现+验证短链,复杂任务增加规划、评审、修复与换将"},
|
|
40
40
|
{"id": "novel", "name": "小说", "icon": "i-book-open", "engine": "review", "builtin": True,
|
|
41
41
|
"manuscript": "manuscript.md",
|
|
42
42
|
"rubric": ["情节", "人物", "文笔", "节奏", "吸引力"],
|
package/app/core/jobs.py
CHANGED
|
@@ -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
|
|
@@ -309,6 +320,18 @@ def _run_job(job):
|
|
|
309
320
|
pass
|
|
310
321
|
finally:
|
|
311
322
|
if run_id:
|
|
323
|
+
# 执行线程退出就是生命周期的最后边界。流水线分支若意外提前 return、
|
|
324
|
+
# 或内部吞掉异常却漏写终态,旧逻辑会让 run/task 永久停在 running。
|
|
325
|
+
try:
|
|
326
|
+
from . import store
|
|
327
|
+
latest = store.get_run(run_id)
|
|
328
|
+
if latest and latest.get("status") == "running":
|
|
329
|
+
store.update_run(
|
|
330
|
+
run_id, expected_status="running", status="failed",
|
|
331
|
+
ended_at=_now(),
|
|
332
|
+
error="执行流程已退出但未写入完成状态,已自动标记失败;可重试")
|
|
333
|
+
except Exception:
|
|
334
|
+
pass
|
|
312
335
|
CANCELS.pop(run_id, None)
|
|
313
336
|
try:
|
|
314
337
|
_maybe_auto_resume(run_id)
|
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 {})
|
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
|
|
|
@@ -944,14 +948,21 @@ def _run_code(run, task, agents, ev, stats, mode):
|
|
|
944
948
|
error="没有可用智能体", ended_at=_now())
|
|
945
949
|
return
|
|
946
950
|
|
|
947
|
-
#
|
|
948
|
-
|
|
951
|
+
# 项目记忆既给规划器,也给快速路径的实现者;不能因省掉独立规划而漏掉。
|
|
952
|
+
user_context_len = len(task.get("context") or "")
|
|
953
|
+
project_memory = _read_project_memory(workdir)
|
|
954
|
+
if project_memory:
|
|
955
|
+
task = dict(task, context=(task.get("context") or "") + "\n\n" + project_memory)
|
|
956
|
+
|
|
957
|
+
# ---- 规划。低风险短任务直接把目标作为单步计划,省掉一次独立模型调用;
|
|
958
|
+
# 附件或较长的用户背景仍先规划,避免快速路径漏读约束。
|
|
959
|
+
fast_path = (mode == "fast" or
|
|
960
|
+
(mode == "auto" and difficulty == "easy"
|
|
961
|
+
and not task.get("attachments")
|
|
962
|
+
and user_context_len < 600))
|
|
963
|
+
planned = False
|
|
964
|
+
if mode == "auto" and not fast_path:
|
|
949
965
|
_wait_gate(run_id, ev)
|
|
950
|
-
# 项目记忆注入(借鉴 agentmemory 持久记忆):同工作目录此前代码任务留下的
|
|
951
|
-
# 架构事实,让规划器不再对代码库一无所知
|
|
952
|
-
_pm = _read_project_memory(workdir)
|
|
953
|
-
if _pm:
|
|
954
|
-
task = dict(task, context=(task.get("context") or "") + "\n\n" + _pm)
|
|
955
966
|
plan_step, plan_log = store.add_step(run_id, "plan", impl["id"], impl.get("label"),
|
|
956
967
|
note=route.get("implementer", ""))
|
|
957
968
|
plan = planner.make_code_plan(_steered_task(run_id, task),
|
|
@@ -967,21 +978,39 @@ def _run_code(run, task, agents, ev, stats, mode):
|
|
|
967
978
|
plan["source"], difficulty,
|
|
968
979
|
";".join(s["title"] for s in plan["steps"])[:160]),
|
|
969
980
|
duration_s=0.1 if impl.get("mode") == "mock" else None)
|
|
981
|
+
planned = True
|
|
982
|
+
elif fast_path:
|
|
983
|
+
plan = {
|
|
984
|
+
"source": "dynamic-fast-path",
|
|
985
|
+
"difficulty": "easy",
|
|
986
|
+
"steps": [{"title": "直接实现并自检", "detail": task["goal"]}],
|
|
987
|
+
"workflow": {
|
|
988
|
+
"review_required": not bool(task.get("verify_command")),
|
|
989
|
+
"max_repair_rounds": 1,
|
|
990
|
+
"allow_switch": False,
|
|
991
|
+
},
|
|
992
|
+
}
|
|
970
993
|
else:
|
|
971
994
|
plan = {"source": "manual", "steps": [{"title": "实现任务", "detail": task["goal"]}]}
|
|
972
|
-
|
|
995
|
+
workflow = task_compile.code_workflow(
|
|
996
|
+
task, difficulty, plan=plan, mode=mode, planned=planned)
|
|
997
|
+
store.update_run(run_id, plan=plan, difficulty=difficulty, workflow=workflow)
|
|
973
998
|
subtasks = plan["steps"]
|
|
974
999
|
# 计划落盘(planning-with-files):磁盘上的计划与 spec/evidence 同居任务档案
|
|
975
1000
|
_write_task_plan(task, workdir, plan)
|
|
976
1001
|
|
|
977
1002
|
# ---- 评审者(有会话延续时评审者仍用新鲜上下文,避免偏见)
|
|
978
|
-
if
|
|
1003
|
+
if not workflow["review_required"]:
|
|
1004
|
+
reviewer = None
|
|
1005
|
+
route["reviewer"] = workflow["reason"]
|
|
1006
|
+
elif mode == "auto":
|
|
979
1007
|
reviewer, route["reviewer"] = router.pick_reviewer(agents, impl, "code", stats)
|
|
980
1008
|
else:
|
|
981
1009
|
reviewer, route["reviewer"] = _pick_reviewer_legacy(agents, impl)
|
|
982
1010
|
_record_actual_route(run_id, task, agents, stats, impl, reviewer=reviewer,
|
|
983
1011
|
implement_reason=route.get("implementer", ""),
|
|
984
|
-
review_reason=route.get("reviewer", "")
|
|
1012
|
+
review_reason=route.get("reviewer", ""),
|
|
1013
|
+
direct=not workflow["review_required"])
|
|
985
1014
|
store.update_run(run_id, route=route)
|
|
986
1015
|
|
|
987
1016
|
def implement_all(impl_agent, prefix_note):
|
|
@@ -1078,7 +1107,8 @@ def _run_code(run, task, agents, ev, stats, mode):
|
|
|
1078
1107
|
run_id, task, agents, stats, other,
|
|
1079
1108
|
implementers=[other], reviewer=reviewer,
|
|
1080
1109
|
implement_reason=note,
|
|
1081
|
-
review_reason=route.get("reviewer", "")
|
|
1110
|
+
review_reason=route.get("reviewer", ""),
|
|
1111
|
+
direct=not workflow["review_required"])
|
|
1082
1112
|
return True
|
|
1083
1113
|
res_err = "%s;换将后仍失败:%s" % (note, (res.get("error") or "")[:200])
|
|
1084
1114
|
else:
|
|
@@ -1091,6 +1121,17 @@ def _run_code(run, task, agents, ev, stats, mode):
|
|
|
1091
1121
|
|
|
1092
1122
|
def review_and_score():
|
|
1093
1123
|
verify_pass, verify_ran = _run_verify(run_id, task, workdir, ev)
|
|
1124
|
+
if not workflow["review_required"]:
|
|
1125
|
+
return {
|
|
1126
|
+
"pass": verify_pass,
|
|
1127
|
+
"scores": {},
|
|
1128
|
+
"issues": ([] if verify_pass else [{
|
|
1129
|
+
"severity": "high", "title": "验证命令未通过",
|
|
1130
|
+
"detail": "动态短链省略模型评审,先修复确定性验证失败。",
|
|
1131
|
+
}]),
|
|
1132
|
+
"summary": "低风险短链:确定性验证%s,模型评审已省略。" %
|
|
1133
|
+
("通过" if verify_pass else "未通过"),
|
|
1134
|
+
}, verify_pass, verify_ran
|
|
1094
1135
|
# gate 验证(借鉴 pi-subagents 的 gate:"npm test"):verify 失败时先跳过
|
|
1095
1136
|
# 模型评审直接进修复轮——评审此时只能复述「验证没过」,白烧一次调用。
|
|
1096
1137
|
# 连续失败 >=2 轮后恢复评审参与诊断(模型能看出编译错误之外的病灶)。
|
|
@@ -1138,7 +1179,8 @@ def _run_code(run, task, agents, ev, stats, mode):
|
|
|
1138
1179
|
implementers=[impl], reviewer=reviewer,
|
|
1139
1180
|
implement_reason="修复轮由 %s 完成" %
|
|
1140
1181
|
(impl.get("label") or impl.get("id")),
|
|
1141
|
-
review_reason=route.get("reviewer", "")
|
|
1182
|
+
review_reason=route.get("reviewer", ""),
|
|
1183
|
+
direct=not workflow["review_required"])
|
|
1142
1184
|
if impl.get("mode") == "mock" and res["ok"]:
|
|
1143
1185
|
pass # mock 不产生真实变更
|
|
1144
1186
|
review_json, verify_pass, verify_ran = review_and_score()
|
|
@@ -1149,10 +1191,10 @@ def _run_code(run, task, agents, ev, stats, mode):
|
|
|
1149
1191
|
"passed": passed})
|
|
1150
1192
|
if passed:
|
|
1151
1193
|
break
|
|
1152
|
-
if round_no <
|
|
1194
|
+
if round_no < workflow["max_repair_rounds"] and not switched:
|
|
1153
1195
|
round_no += 1
|
|
1154
1196
|
continue
|
|
1155
|
-
if mode == "auto" and not switched:
|
|
1197
|
+
if mode == "auto" and workflow["allow_switch"] and not switched:
|
|
1156
1198
|
ex = (impl["id"],)
|
|
1157
1199
|
if impl.get("mode") == "real":
|
|
1158
1200
|
ex += ("mock-a", "mock-b") # 真实实现者失败时不降级到 mock
|
|
@@ -1166,7 +1208,8 @@ def _run_code(run, task, agents, ev, stats, mode):
|
|
|
1166
1208
|
run_id, task, agents, stats, impl,
|
|
1167
1209
|
implementers=[impl], reviewer=reviewer,
|
|
1168
1210
|
implement_reason=note,
|
|
1169
|
-
review_reason=route.get("reviewer", "")
|
|
1211
|
+
review_reason=route.get("reviewer", ""),
|
|
1212
|
+
direct=not workflow["review_required"])
|
|
1170
1213
|
switched = True
|
|
1171
1214
|
round_no += 1
|
|
1172
1215
|
continue
|
|
@@ -1178,18 +1221,27 @@ def _run_code(run, task, agents, ev, stats, mode):
|
|
|
1178
1221
|
verdict = {
|
|
1179
1222
|
"type": "code", "engine": "code", "pass": overall_pass, "mode": mode,
|
|
1180
1223
|
"verify_ran": verify_ran, "verify_pass": verify_pass,
|
|
1181
|
-
"review_pass": bool(review_json.get("pass"))
|
|
1224
|
+
"review_pass": (bool(review_json.get("pass"))
|
|
1225
|
+
if workflow["review_required"] else None),
|
|
1226
|
+
"review_skipped": not workflow["review_required"],
|
|
1182
1227
|
"scores": scores, "overall_score": overall_score,
|
|
1183
1228
|
"issues": review_json.get("issues") or [],
|
|
1184
|
-
"reviewer": reviewer["id"],
|
|
1229
|
+
"reviewer": reviewer["id"] if reviewer else "",
|
|
1185
1230
|
"route": route, "repairs": repairs, "switched": switched,
|
|
1231
|
+
"workflow": workflow,
|
|
1186
1232
|
}
|
|
1187
1233
|
|
|
1188
1234
|
lines = [
|
|
1189
1235
|
"# 代码任务报告:%s" % task["title"], "",
|
|
1190
1236
|
"- 结论:**%s**" % ("✅ 通过" if overall_pass else "❌ 未通过"),
|
|
1191
1237
|
"- 编排模式:%s 实现者:%s 评审者:%s" % (
|
|
1192
|
-
"智能" if mode == "auto" else "手动", impl.get("label"),
|
|
1238
|
+
"智能" if mode == "auto" else "手动", impl.get("label"),
|
|
1239
|
+
reviewer.get("label") if reviewer else "按验证结果省略"),
|
|
1240
|
+
"- 动态步骤:规划 %s;实现 %d 步;验证 %s;评审 %d 人;最多修复 %d 轮;换将 %s" % (
|
|
1241
|
+
"独立执行" if workflow["planning"] == "llm" else "并入实现",
|
|
1242
|
+
workflow["implementation_steps"], "有" if workflow["verification"] else "无",
|
|
1243
|
+
workflow["reviewers"], workflow["max_repair_rounds"],
|
|
1244
|
+
"允许" if workflow["allow_switch"] else "关闭"),
|
|
1193
1245
|
"- 验证命令:%s → %s" % (task.get("verify_command") or "(未配置)",
|
|
1194
1246
|
"通过" if verify_pass else "未通过"),
|
|
1195
1247
|
"- 综合评分:%s 修复/换将:%s" % (
|
|
@@ -1229,7 +1281,8 @@ def _run_code(run, task, agents, ev, stats, mode):
|
|
|
1229
1281
|
summary="代码任务%s(验证%s / 评审%s%s)" % (
|
|
1230
1282
|
"通过" if overall_pass else "未通过",
|
|
1231
1283
|
"通过" if verify_pass else "未通过",
|
|
1232
|
-
"通过" if review_json.get("pass") else "未通过"
|
|
1284
|
+
(("通过" if review_json.get("pass") else "未通过")
|
|
1285
|
+
if workflow["review_required"] else "按策略省略"),
|
|
1233
1286
|
",%d 轮修复" % (len(repairs) - 1) if len(repairs) > 1 else ""),
|
|
1234
1287
|
ended_at=_now())
|
|
1235
1288
|
|
|
@@ -1379,12 +1432,27 @@ def _run_direct(run, task, agents, ev, stats, mode):
|
|
|
1379
1432
|
route = {}
|
|
1380
1433
|
resume_ctx = _valid_resume(task, agents)
|
|
1381
1434
|
bi = None
|
|
1382
|
-
|
|
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")):
|
|
1383
1447
|
try:
|
|
1384
|
-
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"))
|
|
1385
1451
|
except Exception:
|
|
1386
1452
|
bi = None
|
|
1387
1453
|
if bi is not None:
|
|
1454
|
+
bi["reasoning_effort"] = {"low": "low", "standard": "medium", "high": "high"}.get(
|
|
1455
|
+
task.get("thinking"), "medium")
|
|
1388
1456
|
impl = None
|
|
1389
1457
|
route["implementer"] = "CodeBee(%s · %s)" % (bi["provider_name"], bi["model"])
|
|
1390
1458
|
elif resume_ctx is not None:
|
|
@@ -3029,6 +3097,11 @@ def _run_content_review(run, task, agents, ev, stats, mode):
|
|
|
3029
3097
|
step_wd = _resume_workdir(resume_ctx, workdir) if resume_ctx else workdir
|
|
3030
3098
|
difficulty = task.get("difficulty") or (
|
|
3031
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)
|
|
3032
3105
|
|
|
3033
3106
|
# ---- 路由
|
|
3034
3107
|
if resume_ctx is not None:
|
|
@@ -3052,6 +3125,10 @@ def _run_content_review(run, task, agents, ev, stats, mode):
|
|
|
3052
3125
|
else:
|
|
3053
3126
|
critics = _pick_critics_manual(agents, task)
|
|
3054
3127
|
|
|
3128
|
+
critic_pool = list(critics)
|
|
3129
|
+
if mode != "manual":
|
|
3130
|
+
critics = critic_pool[:workflow["reviewers"]]
|
|
3131
|
+
|
|
3055
3132
|
_record_actual_route(run_id, task, agents, stats, impl, critics=critics,
|
|
3056
3133
|
implement_reason=route.get("author", ""),
|
|
3057
3134
|
review_reason=route.get("critics", ""))
|
|
@@ -3059,7 +3136,8 @@ def _run_content_review(run, task, agents, ev, stats, mode):
|
|
|
3059
3136
|
# ---- 规划(小说为模板计划)
|
|
3060
3137
|
_wait_gate(run_id, ev)
|
|
3061
3138
|
plan = planner.make_novel_plan(_steered_task(run_id, task), impl, critics)
|
|
3062
|
-
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)
|
|
3063
3141
|
|
|
3064
3142
|
ms_path = os.path.join(workdir, ms_name)
|
|
3065
3143
|
|
|
@@ -3077,9 +3155,17 @@ def _run_content_review(run, task, agents, ev, stats, mode):
|
|
|
3077
3155
|
|
|
3078
3156
|
# 编排者大纲:只对真实执行有意义;失败静默退回无大纲(喂入带指令的任务副本)
|
|
3079
3157
|
outline = (planner.make_review_outline(_steered_task(run_id, task))
|
|
3080
|
-
if impl.get("mode") != "mock" else None)
|
|
3158
|
+
if workflow["outline"] and impl.get("mode") != "mock" else None)
|
|
3081
3159
|
if outline:
|
|
3082
|
-
|
|
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"]
|
|
3083
3169
|
|
|
3084
3170
|
# 1) 起草
|
|
3085
3171
|
bestof_improvements = "" # 赛马败者精华(真实路径由选择器填充;mock 路径恒空)
|
|
@@ -3228,6 +3314,7 @@ def _run_content_review(run, task, agents, ev, stats, mode):
|
|
|
3228
3314
|
"overall": overall, "mode": mode,
|
|
3229
3315
|
"threshold": threshold, "rounds_used": history_rounds[-1]["round"] if history_rounds else 0,
|
|
3230
3316
|
"scores": final_means, "history": history_rounds, "route": route,
|
|
3317
|
+
"workflow": workflow,
|
|
3231
3318
|
}
|
|
3232
3319
|
|
|
3233
3320
|
# 3) 报告
|
|
@@ -3237,8 +3324,12 @@ def _run_content_review(run, task, agents, ev, stats, mode):
|
|
|
3237
3324
|
% ("✅ 达到发布标准" if publishable else "❌ 未达标,建议再修",
|
|
3238
3325
|
overall, threshold, verdict["rounds_used"]),
|
|
3239
3326
|
"- 编排模式:%s 起草/修订:%s 评审组:%s" % (
|
|
3240
|
-
"
|
|
3241
|
-
|
|
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"])]
|
|
3242
3333
|
if route:
|
|
3243
3334
|
lines.append("")
|
|
3244
3335
|
lines.append("## 路由依据")
|
|
@@ -3589,6 +3680,7 @@ def execute_run(run_id):
|
|
|
3589
3680
|
for _agent in agents:
|
|
3590
3681
|
if isinstance(_agent, dict):
|
|
3591
3682
|
_agent["_dispatch_task_type"] = task.get("type") or "direct"
|
|
3683
|
+
_agent["_thinking"] = task.get("thinking") or "auto"
|
|
3592
3684
|
store.update_run(run_id, route_plan={
|
|
3593
3685
|
"task": task_spec,
|
|
3594
3686
|
"implement": router.route_plan(agents, "implement", task_spec, stats),
|
package/app/core/planner.py
CHANGED
|
@@ -114,11 +114,13 @@ def _log_usage(source, role, task, res, agent=None, tool="", model="", provider=
|
|
|
114
114
|
except Exception:
|
|
115
115
|
pass
|
|
116
116
|
|
|
117
|
-
CODE_PLAN_PROMPT = """你是技术负责人。请把下面的开发目标拆解为 __N__ 个以内、按顺序执行的子任务,
|
|
118
|
-
|
|
119
|
-
{"difficulty": "easy 或 hard", "subtasks": [{"title": "简短标题", "detail": "具体要做什么,给执行工程师的直接指令", "files": ["涉及的文件路径"]}]}
|
|
120
|
-
难度判定:常规增删改查/小函数/格式调整 = easy;跨模块改动/架构调整/复杂算法/安全相关 = hard。
|
|
121
|
-
|
|
117
|
+
CODE_PLAN_PROMPT = """你是技术负责人。请把下面的开发目标拆解为 __N__ 个以内、按顺序执行的子任务,
|
|
118
|
+
并判定任务难度和后续质量步骤。只输出一个 ```json 代码块,不要输出其他内容。JSON 结构:
|
|
119
|
+
{"difficulty": "easy 或 hard", "workflow": {"review_required": true, "max_repair_rounds": 0, "allow_switch": false}, "subtasks": [{"title": "简短标题", "detail": "具体要做什么,给执行工程师的直接指令", "files": ["涉及的文件路径"]}]}
|
|
120
|
+
难度判定:常规增删改查/小函数/格式调整 = easy;跨模块改动/架构调整/复杂算法/安全相关 = hard。
|
|
121
|
+
质量步骤判定:涉及安全、数据迁移、并发、权限、公共 API 或跨模块改动时必须评审;
|
|
122
|
+
有明确自动验收的低风险小改可不做模型评审。max_repair_rounds 取 0-2,只有复杂任务才建议换将。
|
|
123
|
+
子任务粒度要可独立验证;最后一个子任务必须包含整体联调/收尾。
|
|
122
124
|
|
|
123
125
|
## 先探索再计划(重要,借鉴 OpenSpec explore)
|
|
124
126
|
拆解之前先用你的读文件/搜索工具**实际查看工作目录**,找到目标相关的真实文件与函数,再据此拆解。
|
|
@@ -135,9 +137,12 @@ __CONTEXT__
|
|
|
135
137
|
## 验收命令(最终必须通过)
|
|
136
138
|
__VERIFY__"""
|
|
137
139
|
|
|
138
|
-
REVIEW_OUTLINE_PROMPT = """你是内容主编。请为下面的创作任务拟一份写作大纲(要点列表,3-8 条),
|
|
139
|
-
|
|
140
|
-
|
|
140
|
+
REVIEW_OUTLINE_PROMPT = """你是内容主编。请为下面的创作任务拟一份写作大纲(要点列表,3-8 条),
|
|
141
|
+
并按内容风险建议后续质量步骤。
|
|
142
|
+
只输出一个 ```json 代码块,不要输出其他内容。JSON 结构:
|
|
143
|
+
{"outline": ["要点1", "要点2", ...], "workflow": {"reviewers": 1, "review_rounds": 1}}
|
|
144
|
+
reviewers 取 1-2,review_rounds 取 1-3;研究报告、技术方案、长文和高发布门槛应增加,
|
|
145
|
+
普通邮件、短翻译、工作汇报等低风险短内容保持 1。
|
|
141
146
|
|
|
142
147
|
## 创作任务
|
|
143
148
|
__GOAL__
|
|
@@ -403,9 +408,27 @@ def _norm_subtasks(data):
|
|
|
403
408
|
return steps or None
|
|
404
409
|
|
|
405
410
|
|
|
406
|
-
def _plan_difficulty(data):
|
|
411
|
+
def _plan_difficulty(data):
|
|
407
412
|
d = str((data or {}).get("difficulty") or "").lower()
|
|
408
|
-
return d if d in ("easy", "hard") else None
|
|
413
|
+
return d if d in ("easy", "hard") else None
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def _plan_workflow(data):
|
|
417
|
+
"""规范化规划器给出的后续步骤建议;最终安全下限由任务编译器裁决。"""
|
|
418
|
+
raw = (data or {}).get("workflow")
|
|
419
|
+
if not isinstance(raw, dict):
|
|
420
|
+
return None
|
|
421
|
+
out = {}
|
|
422
|
+
if isinstance(raw.get("review_required"), bool):
|
|
423
|
+
out["review_required"] = raw["review_required"]
|
|
424
|
+
try:
|
|
425
|
+
if "max_repair_rounds" in raw:
|
|
426
|
+
out["max_repair_rounds"] = max(0, min(2, int(raw["max_repair_rounds"])))
|
|
427
|
+
except (TypeError, ValueError):
|
|
428
|
+
pass
|
|
429
|
+
if isinstance(raw.get("allow_switch"), bool):
|
|
430
|
+
out["allow_switch"] = raw["allow_switch"]
|
|
431
|
+
return out or None
|
|
409
432
|
|
|
410
433
|
|
|
411
434
|
def _orchestrator():
|
|
@@ -437,9 +460,10 @@ def make_code_plan(task, planner_agent, workdir, ev=None, resume=None, log_path=
|
|
|
437
460
|
_log_usage("plan", "plan", task, res, agent=planner_agent)
|
|
438
461
|
data = runner.extract_json(res.get("text") or "")
|
|
439
462
|
steps = _norm_subtasks(data)
|
|
440
|
-
if steps:
|
|
441
|
-
return {"source": "llm(%s)" % planner_agent["id"], "steps": steps,
|
|
442
|
-
"difficulty": _plan_difficulty(data)
|
|
463
|
+
if steps:
|
|
464
|
+
return {"source": "llm(%s)" % planner_agent["id"], "steps": steps,
|
|
465
|
+
"difficulty": _plan_difficulty(data),
|
|
466
|
+
"workflow": _plan_workflow(data)}
|
|
443
467
|
return _fallback_code_plan(task, "LLM 计划解析失败,退化为单步模板")
|
|
444
468
|
|
|
445
469
|
|
|
@@ -468,8 +492,9 @@ def _orch_code_plan(task, prov, model, log_path=None):
|
|
|
468
492
|
if not steps:
|
|
469
493
|
_append_log(log_path, "编排者计划返回内容无法解析为子任务,回退 CLI")
|
|
470
494
|
return None
|
|
471
|
-
return {"source": "编排者(%s · %s)" % (prov.get("name", prov["id"]), model),
|
|
472
|
-
"steps": steps, "difficulty": _plan_difficulty(data)
|
|
495
|
+
return {"source": "编排者(%s · %s)" % (prov.get("name", prov["id"]), model),
|
|
496
|
+
"steps": steps, "difficulty": _plan_difficulty(data),
|
|
497
|
+
"workflow": _plan_workflow(data)}
|
|
473
498
|
|
|
474
499
|
|
|
475
500
|
def make_review_outline(task):
|
|
@@ -492,8 +517,20 @@ def make_review_outline(task):
|
|
|
492
517
|
if not isinstance(outline, list):
|
|
493
518
|
return None
|
|
494
519
|
items = [str(x).strip()[:120] for x in outline if str(x).strip()][:8]
|
|
495
|
-
|
|
496
|
-
|
|
520
|
+
if not items:
|
|
521
|
+
return None
|
|
522
|
+
workflow = data.get("workflow") if isinstance(data.get("workflow"), dict) else {}
|
|
523
|
+
try:
|
|
524
|
+
reviewers = max(1, min(2, int(workflow.get("reviewers") or 1)))
|
|
525
|
+
except (TypeError, ValueError):
|
|
526
|
+
reviewers = 1
|
|
527
|
+
try:
|
|
528
|
+
review_rounds = max(1, min(3, int(workflow.get("review_rounds") or 1)))
|
|
529
|
+
except (TypeError, ValueError):
|
|
530
|
+
review_rounds = 1
|
|
531
|
+
return {"source": "编排者(%s · %s)" % (prov.get("name", prov["id"]), model),
|
|
532
|
+
"items": items,
|
|
533
|
+
"workflow": {"reviewers": reviewers, "review_rounds": review_rounds}}
|
|
497
534
|
|
|
498
535
|
|
|
499
536
|
def make_novel_plan(task, author, critics):
|