codebee 0.1.28 → 0.1.29
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 +10 -0
- package/README.md +8 -0
- package/app/core/dispatch.py +3 -1
- package/app/core/flows.py +1 -1
- package/app/core/jobs.py +12 -0
- package/app/core/pipeline.py +67 -19
- package/app/core/planner.py +54 -17
- package/app/core/router.py +16 -9
- package/app/core/selfupdate.py +13 -8
- package/app/core/task_compile.py +90 -0
- package/app/main.py +6 -0
- package/app/pet.py +55 -47
- package/app/ui/app.js +33 -10
- package/app/ui/i18n.js +5 -0
- package/app/ui/index.html +6 -5
- package/app/ui/style.css +31 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,16 @@ README 元数据带回,供老版本在「发现新版本」时展示新版更
|
|
|
6
6
|
|
|
7
7
|
## 未发布
|
|
8
8
|
|
|
9
|
+
## v0.1.29(2026-09-21)
|
|
10
|
+
|
|
11
|
+
### ✨ 新功能
|
|
12
|
+
|
|
13
|
+
- 升级后蜜蜂自动换新装:修复「升级后还是旧的丑蜜蜂」——服务重启后旧蜜蜂会自动让位,由新服务拉起带新形象的新蜜蜂(此前旧蜜蜂占着位置永不退场)。
|
|
14
|
+
|
|
15
|
+
### 🛠 问题修复
|
|
16
|
+
|
|
17
|
+
- 去掉蜜蜂底部的任务进度条(真机截图反馈):窗口收紧贴着蜂体,画面更干净。
|
|
18
|
+
|
|
9
19
|
## v0.1.28(2026-09-21)
|
|
10
20
|
|
|
11
21
|
### 🛠 问题修复
|
package/README.md
CHANGED
|
@@ -21,8 +21,16 @@ Kimi Code、MiMo Code、Grok Build、Pi、DeepSeek Harness……),提供
|
|
|
21
21
|
不会把你的任务内容交给任何第三方。
|
|
22
22
|
|
|
23
23
|
<!-- relnotes:start -->
|
|
24
|
+
### ✨ 新功能 · New
|
|
25
|
+
|
|
26
|
+
- 升级后蜜蜂自动换新装:服务重启后旧蜜蜂自动让位,新蜜蜂带新形象上线。
|
|
27
|
+
- After upgrading, the desktop bee swaps itself for the new look: the old bee steps aside when the service restarts.
|
|
28
|
+
|
|
24
29
|
### 🛠 问题修复 · Fixes
|
|
25
30
|
|
|
31
|
+
- 去掉蜜蜂底部的任务进度条,窗口更干净。
|
|
32
|
+
- Removed the task progress bar under the desktop bee for a cleaner look.
|
|
33
|
+
|
|
26
34
|
- 「关于与更新」页的版本信息与更新说明改为左对齐阅读,不再整块居中。
|
|
27
35
|
- The version info and release notes on the About page are now left-aligned for easier reading.
|
|
28
36
|
|
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
|
@@ -309,6 +309,18 @@ def _run_job(job):
|
|
|
309
309
|
pass
|
|
310
310
|
finally:
|
|
311
311
|
if run_id:
|
|
312
|
+
# 执行线程退出就是生命周期的最后边界。流水线分支若意外提前 return、
|
|
313
|
+
# 或内部吞掉异常却漏写终态,旧逻辑会让 run/task 永久停在 running。
|
|
314
|
+
try:
|
|
315
|
+
from . import store
|
|
316
|
+
latest = store.get_run(run_id)
|
|
317
|
+
if latest and latest.get("status") == "running":
|
|
318
|
+
store.update_run(
|
|
319
|
+
run_id, expected_status="running", status="failed",
|
|
320
|
+
ended_at=_now(),
|
|
321
|
+
error="执行流程已退出但未写入完成状态,已自动标记失败;可重试")
|
|
322
|
+
except Exception:
|
|
323
|
+
pass
|
|
312
324
|
CANCELS.pop(run_id, None)
|
|
313
325
|
try:
|
|
314
326
|
_maybe_auto_resume(run_id)
|
package/app/core/pipeline.py
CHANGED
|
@@ -944,14 +944,20 @@ def _run_code(run, task, agents, ev, stats, mode):
|
|
|
944
944
|
error="没有可用智能体", ended_at=_now())
|
|
945
945
|
return
|
|
946
946
|
|
|
947
|
-
#
|
|
948
|
-
|
|
947
|
+
# 项目记忆既给规划器,也给快速路径的实现者;不能因省掉独立规划而漏掉。
|
|
948
|
+
user_context_len = len(task.get("context") or "")
|
|
949
|
+
project_memory = _read_project_memory(workdir)
|
|
950
|
+
if project_memory:
|
|
951
|
+
task = dict(task, context=(task.get("context") or "") + "\n\n" + project_memory)
|
|
952
|
+
|
|
953
|
+
# ---- 规划。低风险短任务直接把目标作为单步计划,省掉一次独立模型调用;
|
|
954
|
+
# 附件或较长的用户背景仍先规划,避免快速路径漏读约束。
|
|
955
|
+
fast_path = (mode == "auto" and difficulty == "easy"
|
|
956
|
+
and not task.get("attachments")
|
|
957
|
+
and user_context_len < 600)
|
|
958
|
+
planned = False
|
|
959
|
+
if mode == "auto" and not fast_path:
|
|
949
960
|
_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
961
|
plan_step, plan_log = store.add_step(run_id, "plan", impl["id"], impl.get("label"),
|
|
956
962
|
note=route.get("implementer", ""))
|
|
957
963
|
plan = planner.make_code_plan(_steered_task(run_id, task),
|
|
@@ -967,21 +973,39 @@ def _run_code(run, task, agents, ev, stats, mode):
|
|
|
967
973
|
plan["source"], difficulty,
|
|
968
974
|
";".join(s["title"] for s in plan["steps"])[:160]),
|
|
969
975
|
duration_s=0.1 if impl.get("mode") == "mock" else None)
|
|
976
|
+
planned = True
|
|
977
|
+
elif fast_path:
|
|
978
|
+
plan = {
|
|
979
|
+
"source": "dynamic-fast-path",
|
|
980
|
+
"difficulty": "easy",
|
|
981
|
+
"steps": [{"title": "直接实现并自检", "detail": task["goal"]}],
|
|
982
|
+
"workflow": {
|
|
983
|
+
"review_required": not bool(task.get("verify_command")),
|
|
984
|
+
"max_repair_rounds": 1,
|
|
985
|
+
"allow_switch": False,
|
|
986
|
+
},
|
|
987
|
+
}
|
|
970
988
|
else:
|
|
971
989
|
plan = {"source": "manual", "steps": [{"title": "实现任务", "detail": task["goal"]}]}
|
|
972
|
-
|
|
990
|
+
workflow = task_compile.code_workflow(
|
|
991
|
+
task, difficulty, plan=plan, mode=mode, planned=planned)
|
|
992
|
+
store.update_run(run_id, plan=plan, difficulty=difficulty, workflow=workflow)
|
|
973
993
|
subtasks = plan["steps"]
|
|
974
994
|
# 计划落盘(planning-with-files):磁盘上的计划与 spec/evidence 同居任务档案
|
|
975
995
|
_write_task_plan(task, workdir, plan)
|
|
976
996
|
|
|
977
997
|
# ---- 评审者(有会话延续时评审者仍用新鲜上下文,避免偏见)
|
|
978
|
-
if
|
|
998
|
+
if not workflow["review_required"]:
|
|
999
|
+
reviewer = None
|
|
1000
|
+
route["reviewer"] = workflow["reason"]
|
|
1001
|
+
elif mode == "auto":
|
|
979
1002
|
reviewer, route["reviewer"] = router.pick_reviewer(agents, impl, "code", stats)
|
|
980
1003
|
else:
|
|
981
1004
|
reviewer, route["reviewer"] = _pick_reviewer_legacy(agents, impl)
|
|
982
1005
|
_record_actual_route(run_id, task, agents, stats, impl, reviewer=reviewer,
|
|
983
1006
|
implement_reason=route.get("implementer", ""),
|
|
984
|
-
review_reason=route.get("reviewer", "")
|
|
1007
|
+
review_reason=route.get("reviewer", ""),
|
|
1008
|
+
direct=not workflow["review_required"])
|
|
985
1009
|
store.update_run(run_id, route=route)
|
|
986
1010
|
|
|
987
1011
|
def implement_all(impl_agent, prefix_note):
|
|
@@ -1078,7 +1102,8 @@ def _run_code(run, task, agents, ev, stats, mode):
|
|
|
1078
1102
|
run_id, task, agents, stats, other,
|
|
1079
1103
|
implementers=[other], reviewer=reviewer,
|
|
1080
1104
|
implement_reason=note,
|
|
1081
|
-
review_reason=route.get("reviewer", "")
|
|
1105
|
+
review_reason=route.get("reviewer", ""),
|
|
1106
|
+
direct=not workflow["review_required"])
|
|
1082
1107
|
return True
|
|
1083
1108
|
res_err = "%s;换将后仍失败:%s" % (note, (res.get("error") or "")[:200])
|
|
1084
1109
|
else:
|
|
@@ -1091,6 +1116,17 @@ def _run_code(run, task, agents, ev, stats, mode):
|
|
|
1091
1116
|
|
|
1092
1117
|
def review_and_score():
|
|
1093
1118
|
verify_pass, verify_ran = _run_verify(run_id, task, workdir, ev)
|
|
1119
|
+
if not workflow["review_required"]:
|
|
1120
|
+
return {
|
|
1121
|
+
"pass": verify_pass,
|
|
1122
|
+
"scores": {},
|
|
1123
|
+
"issues": ([] if verify_pass else [{
|
|
1124
|
+
"severity": "high", "title": "验证命令未通过",
|
|
1125
|
+
"detail": "动态短链省略模型评审,先修复确定性验证失败。",
|
|
1126
|
+
}]),
|
|
1127
|
+
"summary": "低风险短链:确定性验证%s,模型评审已省略。" %
|
|
1128
|
+
("通过" if verify_pass else "未通过"),
|
|
1129
|
+
}, verify_pass, verify_ran
|
|
1094
1130
|
# gate 验证(借鉴 pi-subagents 的 gate:"npm test"):verify 失败时先跳过
|
|
1095
1131
|
# 模型评审直接进修复轮——评审此时只能复述「验证没过」,白烧一次调用。
|
|
1096
1132
|
# 连续失败 >=2 轮后恢复评审参与诊断(模型能看出编译错误之外的病灶)。
|
|
@@ -1138,7 +1174,8 @@ def _run_code(run, task, agents, ev, stats, mode):
|
|
|
1138
1174
|
implementers=[impl], reviewer=reviewer,
|
|
1139
1175
|
implement_reason="修复轮由 %s 完成" %
|
|
1140
1176
|
(impl.get("label") or impl.get("id")),
|
|
1141
|
-
review_reason=route.get("reviewer", "")
|
|
1177
|
+
review_reason=route.get("reviewer", ""),
|
|
1178
|
+
direct=not workflow["review_required"])
|
|
1142
1179
|
if impl.get("mode") == "mock" and res["ok"]:
|
|
1143
1180
|
pass # mock 不产生真实变更
|
|
1144
1181
|
review_json, verify_pass, verify_ran = review_and_score()
|
|
@@ -1149,10 +1186,10 @@ def _run_code(run, task, agents, ev, stats, mode):
|
|
|
1149
1186
|
"passed": passed})
|
|
1150
1187
|
if passed:
|
|
1151
1188
|
break
|
|
1152
|
-
if round_no <
|
|
1189
|
+
if round_no < workflow["max_repair_rounds"] and not switched:
|
|
1153
1190
|
round_no += 1
|
|
1154
1191
|
continue
|
|
1155
|
-
if mode == "auto" and not switched:
|
|
1192
|
+
if mode == "auto" and workflow["allow_switch"] and not switched:
|
|
1156
1193
|
ex = (impl["id"],)
|
|
1157
1194
|
if impl.get("mode") == "real":
|
|
1158
1195
|
ex += ("mock-a", "mock-b") # 真实实现者失败时不降级到 mock
|
|
@@ -1166,7 +1203,8 @@ def _run_code(run, task, agents, ev, stats, mode):
|
|
|
1166
1203
|
run_id, task, agents, stats, impl,
|
|
1167
1204
|
implementers=[impl], reviewer=reviewer,
|
|
1168
1205
|
implement_reason=note,
|
|
1169
|
-
review_reason=route.get("reviewer", "")
|
|
1206
|
+
review_reason=route.get("reviewer", ""),
|
|
1207
|
+
direct=not workflow["review_required"])
|
|
1170
1208
|
switched = True
|
|
1171
1209
|
round_no += 1
|
|
1172
1210
|
continue
|
|
@@ -1178,18 +1216,27 @@ def _run_code(run, task, agents, ev, stats, mode):
|
|
|
1178
1216
|
verdict = {
|
|
1179
1217
|
"type": "code", "engine": "code", "pass": overall_pass, "mode": mode,
|
|
1180
1218
|
"verify_ran": verify_ran, "verify_pass": verify_pass,
|
|
1181
|
-
"review_pass": bool(review_json.get("pass"))
|
|
1219
|
+
"review_pass": (bool(review_json.get("pass"))
|
|
1220
|
+
if workflow["review_required"] else None),
|
|
1221
|
+
"review_skipped": not workflow["review_required"],
|
|
1182
1222
|
"scores": scores, "overall_score": overall_score,
|
|
1183
1223
|
"issues": review_json.get("issues") or [],
|
|
1184
|
-
"reviewer": reviewer["id"],
|
|
1224
|
+
"reviewer": reviewer["id"] if reviewer else "",
|
|
1185
1225
|
"route": route, "repairs": repairs, "switched": switched,
|
|
1226
|
+
"workflow": workflow,
|
|
1186
1227
|
}
|
|
1187
1228
|
|
|
1188
1229
|
lines = [
|
|
1189
1230
|
"# 代码任务报告:%s" % task["title"], "",
|
|
1190
1231
|
"- 结论:**%s**" % ("✅ 通过" if overall_pass else "❌ 未通过"),
|
|
1191
1232
|
"- 编排模式:%s 实现者:%s 评审者:%s" % (
|
|
1192
|
-
"智能" if mode == "auto" else "手动", impl.get("label"),
|
|
1233
|
+
"智能" if mode == "auto" else "手动", impl.get("label"),
|
|
1234
|
+
reviewer.get("label") if reviewer else "按验证结果省略"),
|
|
1235
|
+
"- 动态步骤:规划 %s;实现 %d 步;验证 %s;评审 %d 人;最多修复 %d 轮;换将 %s" % (
|
|
1236
|
+
"独立执行" if workflow["planning"] == "llm" else "并入实现",
|
|
1237
|
+
workflow["implementation_steps"], "有" if workflow["verification"] else "无",
|
|
1238
|
+
workflow["reviewers"], workflow["max_repair_rounds"],
|
|
1239
|
+
"允许" if workflow["allow_switch"] else "关闭"),
|
|
1193
1240
|
"- 验证命令:%s → %s" % (task.get("verify_command") or "(未配置)",
|
|
1194
1241
|
"通过" if verify_pass else "未通过"),
|
|
1195
1242
|
"- 综合评分:%s 修复/换将:%s" % (
|
|
@@ -1229,7 +1276,8 @@ def _run_code(run, task, agents, ev, stats, mode):
|
|
|
1229
1276
|
summary="代码任务%s(验证%s / 评审%s%s)" % (
|
|
1230
1277
|
"通过" if overall_pass else "未通过",
|
|
1231
1278
|
"通过" if verify_pass else "未通过",
|
|
1232
|
-
"通过" if review_json.get("pass") else "未通过"
|
|
1279
|
+
(("通过" if review_json.get("pass") else "未通过")
|
|
1280
|
+
if workflow["review_required"] else "按策略省略"),
|
|
1233
1281
|
",%d 轮修复" % (len(repairs) - 1) if len(repairs) > 1 else ""),
|
|
1234
1282
|
ended_at=_now())
|
|
1235
1283
|
|
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):
|
package/app/core/router.py
CHANGED
|
@@ -11,6 +11,7 @@ CAPABILITY = {
|
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
MAX_REPAIR_ROUNDS = 2 # 自动修复循环上限
|
|
14
|
+
AUTO_CRITIC_LIMIT = 2 # 自动内容评审只取综合分前两名;手工模式仍尊重完整名单
|
|
14
15
|
|
|
15
16
|
|
|
16
17
|
def _task_type(ttype):
|
|
@@ -62,7 +63,9 @@ def _online_bonus(agent, role, ttype):
|
|
|
62
63
|
rate = float(metrics.get("success_rate") or 0.0)
|
|
63
64
|
success_score = max(-8.0, min(8.0, (rate - 0.75) * 24.0))
|
|
64
65
|
p95 = max(0.0, float(metrics.get("p95_duration_s") or 0.0))
|
|
65
|
-
|
|
66
|
+
# 旧上限 -6 抵不过 CLI 静态能力 10 分差,P95 两三分钟的模型仍会
|
|
67
|
+
# 压过十几秒的健康模型。加大惩罚,让真实延迟足以改变自动选择。
|
|
68
|
+
latency_score = -min(18.0, max(0.0, (p95 - 30.0) / 7.0))
|
|
66
69
|
cost = max(0.0, float(metrics.get("avg_cost_usd") or 0.0))
|
|
67
70
|
cost_score = -min(4.0, max(0.0, (cost - 0.01) / 0.01))
|
|
68
71
|
total = round(success_score + latency_score + cost_score, 1)
|
|
@@ -199,14 +202,18 @@ def pick_reviewer(agents, impl, ttype, stats=None):
|
|
|
199
202
|
return impl, "(自评:仅有实现者一个智能体可用)"
|
|
200
203
|
|
|
201
204
|
|
|
202
|
-
def pick_critics(agents, ttype, stats=None, impl=None):
|
|
203
|
-
"""
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
205
|
+
def pick_critics(agents, ttype, stats=None, impl=None):
|
|
206
|
+
"""自动内容评审组选综合分前两名(排除作者本人)。
|
|
207
|
+
|
|
208
|
+
旧逻辑让全部已启用 CLI 串行参与每章和全局评审;一条慢/失效链就会把
|
|
209
|
+
任务拖长数分钟。两名保留交叉判断并封顶默认调用数;手工模式不走这里。
|
|
210
|
+
"""
|
|
211
|
+
stats = stats or history.agent_stats()
|
|
212
|
+
real = [a for a in agents if a.get("mode") == "real" and (not impl or a["id"] != impl["id"])]
|
|
213
|
+
if real:
|
|
214
|
+
ranked = sorted(real, key=lambda a: score(a, "review", ttype, stats)[0], reverse=True)
|
|
215
|
+
real = ranked[:AUTO_CRITIC_LIMIT]
|
|
216
|
+
note = "按成功率与延迟选择前 %d 名评审(共 %d 名候选)" % (len(real), len(ranked))
|
|
210
217
|
if impl and impl.get("mode") == "real" \
|
|
211
218
|
and all(a.get("kind") == impl.get("kind") for a in real):
|
|
212
219
|
note += ";评审组与作者同为 %s,建议启用其他厂商 CLI 交叉评审" % impl.get("kind")
|
package/app/core/selfupdate.py
CHANGED
|
@@ -188,16 +188,21 @@ def apply_upgrade(port=None):
|
|
|
188
188
|
return {"error": "升级任务创建失败,请稍后重试"}
|
|
189
189
|
try:
|
|
190
190
|
jobs.enqueue({"kind": "selfupgrade", "run_id": run["id"]})
|
|
191
|
-
except Exception:
|
|
192
|
-
# run 已持久化;启动失败时显式收口,版本页不能停在误导性的待启动状态。
|
|
193
|
-
log.exception("selfupdate: 升级任务启动失败 run=%s", run["id"])
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
191
|
+
except Exception as exc:
|
|
192
|
+
# run 已持久化;启动失败时显式收口,版本页不能停在误导性的待启动状态。
|
|
193
|
+
log.exception("selfupdate: 升级任务启动失败 run=%s", run["id"])
|
|
194
|
+
if isinstance(exc, jobs.JobsBusyError):
|
|
195
|
+
message = "升级未启动:%s" % str(exc)
|
|
196
|
+
else:
|
|
197
|
+
# 仅并发保护类错误可直接展示;其他内部异常继续隐藏实现细节。
|
|
198
|
+
message = "升级任务启动失败,本次未排队,请稍后重试"
|
|
199
|
+
try:
|
|
200
|
+
store.update_run(run["id"], status="failed",
|
|
201
|
+
error=message,
|
|
202
|
+
ended_at=time.strftime("%Y-%m-%d %H:%M:%S"))
|
|
198
203
|
except Exception:
|
|
199
204
|
log.exception("selfupdate: 升级运行失败收口失败 run=%s", run["id"])
|
|
200
|
-
return {"error":
|
|
205
|
+
return {"error": message, "run_id": run["id"]}
|
|
201
206
|
return {"run_id": run["id"]}
|
|
202
207
|
|
|
203
208
|
|
package/app/core/task_compile.py
CHANGED
|
@@ -85,3 +85,93 @@ def summary(spec):
|
|
|
85
85
|
spec.get("type") or "direct", spec.get("engine") or "direct",
|
|
86
86
|
spec.get("difficulty") or "default",
|
|
87
87
|
"、".join(spec.get("capabilities") or []) or "通用")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def code_workflow(task, difficulty, plan=None, mode="auto", planned=True):
|
|
91
|
+
"""把难度、验收能力和规划建议收敛成代码任务的实际步骤策略。
|
|
92
|
+
|
|
93
|
+
自动模式允许低风险任务走短链;手动模式与高风险任务保留完整质量门禁。
|
|
94
|
+
规划器只能在安全边界内减少步骤,不能让无确定性验证的任务跳过评审。
|
|
95
|
+
"""
|
|
96
|
+
task = task if isinstance(task, Mapping) else {}
|
|
97
|
+
rec = (plan or {}).get("workflow") if isinstance(plan, Mapping) else None
|
|
98
|
+
rec = rec if isinstance(rec, Mapping) else {}
|
|
99
|
+
has_verify = bool(str(task.get("verify_command") or "").strip())
|
|
100
|
+
easy_auto = mode == "auto" and difficulty == "easy"
|
|
101
|
+
|
|
102
|
+
review_required = True
|
|
103
|
+
max_repairs = 2
|
|
104
|
+
allow_switch = True
|
|
105
|
+
reason = "复杂或手动任务使用完整质量门禁"
|
|
106
|
+
if easy_auto:
|
|
107
|
+
review_required = not has_verify
|
|
108
|
+
max_repairs = 1
|
|
109
|
+
allow_switch = False
|
|
110
|
+
reason = ("低风险任务以确定性验证代替模型评审"
|
|
111
|
+
if has_verify else "低风险任务无验证命令,保留一次模型评审")
|
|
112
|
+
if isinstance(rec.get("review_required"), bool):
|
|
113
|
+
review_required = rec["review_required"] or not has_verify
|
|
114
|
+
try:
|
|
115
|
+
if "max_repair_rounds" in rec:
|
|
116
|
+
max_repairs = max(0, min(1, int(rec["max_repair_rounds"])))
|
|
117
|
+
except (TypeError, ValueError):
|
|
118
|
+
pass
|
|
119
|
+
elif mode == "auto" and difficulty == "hard":
|
|
120
|
+
try:
|
|
121
|
+
if "max_repair_rounds" in rec:
|
|
122
|
+
max_repairs = max(1, min(2, int(rec["max_repair_rounds"])))
|
|
123
|
+
except (TypeError, ValueError):
|
|
124
|
+
pass
|
|
125
|
+
if isinstance(rec.get("allow_switch"), bool):
|
|
126
|
+
allow_switch = rec["allow_switch"]
|
|
127
|
+
reason = "复杂任务保留规划、验证与模型评审"
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
"planning": "llm" if planned else "inline",
|
|
131
|
+
"implementation_steps": len((plan or {}).get("steps") or []) or 1,
|
|
132
|
+
"verification": has_verify,
|
|
133
|
+
"review_required": review_required,
|
|
134
|
+
"reviewers": 1 if review_required else 0,
|
|
135
|
+
"max_repair_rounds": max_repairs,
|
|
136
|
+
"allow_switch": allow_switch,
|
|
137
|
+
"reason": reason,
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def content_workflow(task, difficulty, plan=None, mode="auto"):
|
|
142
|
+
"""为非连载内容任务生成动态大纲/评审/修订策略。"""
|
|
143
|
+
task = task if isinstance(task, Mapping) else {}
|
|
144
|
+
ttype = str(task.get("type") or "doc")
|
|
145
|
+
requested_rounds = max(1, min(5, int(task.get("rounds") or 2)))
|
|
146
|
+
rec = (plan or {}).get("workflow") if isinstance(plan, Mapping) else None
|
|
147
|
+
rec = rec if isinstance(rec, Mapping) else {}
|
|
148
|
+
light_types = {"email", "weekly_report", "translation"}
|
|
149
|
+
deep_types = {"novel", "research", "tech_proposal"}
|
|
150
|
+
|
|
151
|
+
if mode != "auto":
|
|
152
|
+
return {"outline": True, "reviewers": 0,
|
|
153
|
+
"review_rounds": requested_rounds,
|
|
154
|
+
"reason": "手动模式尊重用户指定的评审组与轮数"}
|
|
155
|
+
|
|
156
|
+
if ttype in light_types and difficulty != "hard":
|
|
157
|
+
return {"outline": False, "reviewers": 1, "review_rounds": 1,
|
|
158
|
+
"reason": "轻量内容省略独立大纲,只做一次快速评审"}
|
|
159
|
+
|
|
160
|
+
outline = ttype in deep_types or difficulty == "hard"
|
|
161
|
+
reviewers = 2 if outline else 1
|
|
162
|
+
review_rounds = min(requested_rounds, 2 if outline else 1)
|
|
163
|
+
try:
|
|
164
|
+
if "reviewers" in rec:
|
|
165
|
+
reviewers = max(1, min(2, int(rec["reviewers"])))
|
|
166
|
+
if "review_rounds" in rec:
|
|
167
|
+
review_rounds = max(1, min(requested_rounds, 3,
|
|
168
|
+
int(rec["review_rounds"])))
|
|
169
|
+
except (TypeError, ValueError):
|
|
170
|
+
pass
|
|
171
|
+
# 深度类型至少保留一轮评审,但允许规划器把双评审降成一名;高门槛强制双评审。
|
|
172
|
+
if float(task.get("threshold") or 7.0) >= 8.5:
|
|
173
|
+
reviewers = 2
|
|
174
|
+
return {"outline": outline, "reviewers": reviewers,
|
|
175
|
+
"review_rounds": review_rounds,
|
|
176
|
+
"reason": ("深度内容保留大纲和多轮质量门禁" if outline
|
|
177
|
+
else "标准内容使用单评审短链")}
|
package/app/main.py
CHANGED
|
@@ -52,6 +52,7 @@ def _utf8_bytes(data):
|
|
|
52
52
|
return data.decode("utf-8", "replace").encode("utf-8")
|
|
53
53
|
|
|
54
54
|
PORT = 8765 # main() 启动时更新;/api/connect 组装扫码地址用
|
|
55
|
+
_BOOT_TS = time.time() # 服务进程启动时间戳:/api/pet_state 下发,蜜蜂据此识别「服务换人了」并让位
|
|
55
56
|
|
|
56
57
|
# 写接口统一限制 JSON 请求体,避免误传文件或异常客户端把 worker 线程和
|
|
57
58
|
# 内存拖垮。16 MiB 足够覆盖任务上下文、故事圣经和附件清单(附件本体走
|
|
@@ -1468,6 +1469,7 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
1468
1469
|
「刚完工/刚出事」。设置随响应回传,蜜蜂轮询到 pet_enabled=false 自行
|
|
1469
1470
|
退出——关闭桌宠不需要专门的信号通道。
|
|
1470
1471
|
"""
|
|
1472
|
+
global _BOOT_TS
|
|
1471
1473
|
st = settings.load()
|
|
1472
1474
|
rows = []
|
|
1473
1475
|
n_run = n_q = 0
|
|
@@ -1500,6 +1502,10 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
1500
1502
|
dg = {"unseen": 0, "latest": None}
|
|
1501
1503
|
return self._json(200, {
|
|
1502
1504
|
"ok": True, "ts": int(time.time()), "port": PORT,
|
|
1505
|
+
# boot:本服务进程的启动时间戳。蜜蜂记住首次见到的值,值变了说明
|
|
1506
|
+
# 服务换了个进程(升级重启)——旧蜜蜂自行退场让新服务 spawn 新蜜蜂
|
|
1507
|
+
# (旧形象常驻的根因:同端口轮询永远正常,旧进程从不自离)。
|
|
1508
|
+
"boot": _BOOT_TS,
|
|
1503
1509
|
"settings": {"pet_enabled": bool(st.get("pet_enabled", True)),
|
|
1504
1510
|
"pet_mode": str(st.get("pet_mode") or "always"),
|
|
1505
1511
|
"pet_skin": str(st.get("pet_skin") or "plush")},
|
package/app/pet.py
CHANGED
|
@@ -82,6 +82,8 @@ def parse_snapshot(raw):
|
|
|
82
82
|
"error": str(t.get("error") or ""),
|
|
83
83
|
})
|
|
84
84
|
return {
|
|
85
|
+
# boot:服务进程启动标识(值变化=服务换人,本宠让位给新服务的蜜蜂)
|
|
86
|
+
"boot": raw.get("boot") or None,
|
|
85
87
|
"settings": {
|
|
86
88
|
"pet_enabled": bool(st.get("pet_enabled", True)),
|
|
87
89
|
"pet_mode": str(st.get("pet_mode") or "always"),
|
|
@@ -309,7 +311,6 @@ KEY_RGB = (1, 2, 3)
|
|
|
309
311
|
WIN_W, WIN_H = 170, 150 # 手绘回落模式的窗口尺寸
|
|
310
312
|
SPRITE_DIR = Path(__file__).resolve().parent
|
|
311
313
|
SPRITE_DISP_H = 112 # 精灵显示高度(宽等比)
|
|
312
|
-
PROG_H = 16 # 底部进度条预留高度(有进度时才显示)
|
|
313
314
|
|
|
314
315
|
|
|
315
316
|
def _pid_alive(pid):
|
|
@@ -371,6 +372,9 @@ _BROWSER_EXES = {
|
|
|
371
372
|
}
|
|
372
373
|
|
|
373
374
|
|
|
375
|
+
petWindow = None
|
|
376
|
+
|
|
377
|
+
|
|
374
378
|
def _window_process_name(hwnd):
|
|
375
379
|
"""取顶层窗口所属进程的可执行文件名(小写);失败返回空串。"""
|
|
376
380
|
try:
|
|
@@ -438,6 +442,38 @@ def _focus_codebee_window(ignore_hwnd=None):
|
|
|
438
442
|
return False
|
|
439
443
|
|
|
440
444
|
|
|
445
|
+
def _show_pet_window(ignore_hwnd=None):
|
|
446
|
+
"""复用桌宠拉起的 CodeBee 浏览器窗口;窗口已关闭时返回 False。"""
|
|
447
|
+
global petWindow
|
|
448
|
+
try:
|
|
449
|
+
if os.name != "nt":
|
|
450
|
+
return False
|
|
451
|
+
import ctypes
|
|
452
|
+
user32 = ctypes.windll.user32
|
|
453
|
+
if petWindow and user32.IsWindow(petWindow):
|
|
454
|
+
if user32.IsIconic(petWindow):
|
|
455
|
+
user32.ShowWindow(petWindow, 9) # SW_RESTORE
|
|
456
|
+
user32.SetForegroundWindow(petWindow)
|
|
457
|
+
return True
|
|
458
|
+
petWindow = None
|
|
459
|
+
hwnd = _find_codebee_window(ignore_hwnd)
|
|
460
|
+
if not hwnd:
|
|
461
|
+
return False
|
|
462
|
+
petWindow = hwnd
|
|
463
|
+
if user32.IsIconic(hwnd):
|
|
464
|
+
user32.ShowWindow(hwnd, 9)
|
|
465
|
+
user32.SetForegroundWindow(hwnd)
|
|
466
|
+
return True
|
|
467
|
+
except Exception:
|
|
468
|
+
return False
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
def openPetWindow(port, ignore_hwnd=None):
|
|
472
|
+
if os.name == "nt" and _show_pet_window(ignore_hwnd):
|
|
473
|
+
return
|
|
474
|
+
webbrowser.open("http://127.0.0.1:%d" % port)
|
|
475
|
+
|
|
476
|
+
|
|
441
477
|
class PetApp:
|
|
442
478
|
"""蜜蜂窗口:蜜蜂本体(精灵/手绘)+ 状态机动画 + 悬停任务清单 + 右键菜单。"""
|
|
443
479
|
|
|
@@ -485,6 +521,7 @@ class PetApp:
|
|
|
485
521
|
# 运行态
|
|
486
522
|
self.snap = None
|
|
487
523
|
self.miss = 0
|
|
524
|
+
self.boot = None # 首次见到的服务 boot 标识;变化=服务换进程,本宠让位
|
|
488
525
|
self.prev_active = set()
|
|
489
526
|
self.mood_kind, self.mood_until = "", 0.0
|
|
490
527
|
self.last_busy = time.time()
|
|
@@ -638,18 +675,19 @@ class PetApp:
|
|
|
638
675
|
def _size_for(self, frames):
|
|
639
676
|
fw = max(f.width() for lst in frames.values() for f in lst)
|
|
640
677
|
fh = max(f.height() for lst in frames.values() for f in lst)
|
|
641
|
-
return int(fw) + 8, int(fh) +
|
|
678
|
+
return int(fw) + 8, int(fh) + 4
|
|
642
679
|
|
|
643
680
|
# ---- 蜜蜂绘制(精灵模式:贴图 + 状态点缀画件)----
|
|
644
|
-
#
|
|
645
|
-
#
|
|
681
|
+
# 用户拍板去掉:右上徽章、左侧速度线、底部影子(真机截图圈删)、
|
|
682
|
+
# 底部任务进度条(2026-09-21 真机截图圈删)——
|
|
683
|
+
# 窗口贴着蜂体开,点缀只留睡觉 Zzz / 庆祝星光。
|
|
646
684
|
def _build_sprite(self):
|
|
647
685
|
cv = self.cv
|
|
648
|
-
cx, cy = self.win_w // 2,
|
|
686
|
+
cx, cy = self.win_w // 2, self.win_h // 2 + 2
|
|
649
687
|
self._spr_c = (cx, cy)
|
|
650
688
|
self.spr = cv.create_image(cx, cy, image=self.frames["alert"][0],
|
|
651
689
|
anchor="center")
|
|
652
|
-
hw, hh = self.win_w / 2,
|
|
690
|
+
hw, hh = self.win_w / 2, self.win_h / 2
|
|
653
691
|
self.zzz = [cv.create_text(cx + hw - 12, cy - hh + 14, text="z",
|
|
654
692
|
fill="#8A93A6",
|
|
655
693
|
font=("Segoe UI", 11, "italic bold")),
|
|
@@ -671,15 +709,6 @@ class PetApp:
|
|
|
671
709
|
for grp in (self.zzz, self.spark):
|
|
672
710
|
for i in grp:
|
|
673
711
|
cv.itemconfigure(i, state="hidden")
|
|
674
|
-
# 底部进度条(工作且有步骤进度才出现)
|
|
675
|
-
self.pbar_bg = cv.create_rectangle(cx - 34, self.win_h - 7,
|
|
676
|
-
cx + 34, self.win_h - 2,
|
|
677
|
-
fill="#202A36", outline="#3A4654")
|
|
678
|
-
self.pbar_fg = cv.create_rectangle(cx - 33, self.win_h - 6,
|
|
679
|
-
cx - 32, self.win_h - 3,
|
|
680
|
-
fill="#7ED07E", outline="")
|
|
681
|
-
cv.itemconfigure(self.pbar_bg, state="hidden")
|
|
682
|
-
cv.itemconfigure(self.pbar_fg, state="hidden")
|
|
683
712
|
self.ox = self.oy = 0.0
|
|
684
713
|
self._t0 = time.time()
|
|
685
714
|
|
|
@@ -867,14 +896,6 @@ class PetApp:
|
|
|
867
896
|
self._palette = {i: cv.itemcget(i, "fill")
|
|
868
897
|
for i in (self.body, self.belly)}
|
|
869
898
|
|
|
870
|
-
# —— 贴地件:进度条(用户拍板去掉影子)——
|
|
871
|
-
self.pbar_bg = cv.create_rectangle(46, 139, 128, 147, fill="#202A36",
|
|
872
|
-
outline="#3A4654")
|
|
873
|
-
self.pbar_fg = cv.create_rectangle(47, 140, 48, 146, fill="#7ED07E",
|
|
874
|
-
outline="")
|
|
875
|
-
cv.itemconfigure(self.pbar_bg, state="hidden")
|
|
876
|
-
cv.itemconfigure(self.pbar_fg, state="hidden")
|
|
877
|
-
|
|
878
899
|
# 动画游标
|
|
879
900
|
self.ox = self.oy = 0.0
|
|
880
901
|
self._t0 = time.time()
|
|
@@ -923,26 +944,9 @@ class PetApp:
|
|
|
923
944
|
cv.itemconfigure(self.body, fill=self._palette[self.body])
|
|
924
945
|
cv.itemconfigure(self.belly, fill=self._palette[self.belly])
|
|
925
946
|
cv.itemconfigure(self.gloss, state="normal")
|
|
926
|
-
# —— 共同:Zzz /
|
|
947
|
+
# —— 共同:Zzz / 星光(徽章、速度线、进度条均已按用户要求移除)——
|
|
927
948
|
self._show(self.zzz, False)
|
|
928
949
|
self._show(self.spark, False)
|
|
929
|
-
prog = []
|
|
930
|
-
if st == "work" and self.snap:
|
|
931
|
-
prog = [t for t in self.snap["tasks"]
|
|
932
|
-
if t["run_status"] == "running" and t["steps_total"]]
|
|
933
|
-
if prog:
|
|
934
|
-
done = sum(t["steps_done"] for t in prog)
|
|
935
|
-
total = sum(t["steps_total"] for t in prog)
|
|
936
|
-
ratio = max(0.0, min(1.0, done / float(total)))
|
|
937
|
-
x0 = (self.win_w // 2) - 34
|
|
938
|
-
cv.itemconfigure(self.pbar_bg, state="normal")
|
|
939
|
-
cv.itemconfigure(self.pbar_fg, state="normal")
|
|
940
|
-
cv.coords(self.pbar_bg, x0, self.win_h - 7, x0 + 68,
|
|
941
|
-
self.win_h - 2)
|
|
942
|
-
cv.coords(self.pbar_fg, x0 + 1, self.win_h - 6,
|
|
943
|
-
x0 + 1 + 66 * ratio, self.win_h - 3)
|
|
944
|
-
else:
|
|
945
|
-
self._show([self.pbar_bg, self.pbar_fg], False)
|
|
946
950
|
|
|
947
951
|
def _post_settings(self, body):
|
|
948
952
|
"""合并并串行发送设置,保证快速连点时最后一次选择最终生效。"""
|
|
@@ -1029,6 +1033,15 @@ class PetApp:
|
|
|
1029
1033
|
else:
|
|
1030
1034
|
self.miss = 0
|
|
1031
1035
|
if snap is not None:
|
|
1036
|
+
# 服务换人检测(升级重启同端口场景):boot 与首次见到的不一致,
|
|
1037
|
+
# 说明响应来自新进程——本进程是旧代码的遗老,立即让位,新服务的
|
|
1038
|
+
# 看护会 spawn 带新形象的新蜜蜂(旧形象常驻的根因修复)。
|
|
1039
|
+
boot = snap.get("boot")
|
|
1040
|
+
if boot:
|
|
1041
|
+
if self.boot is None:
|
|
1042
|
+
self.boot = boot
|
|
1043
|
+
elif boot != self.boot:
|
|
1044
|
+
return self._bye()
|
|
1032
1045
|
self._reconcile_desired_settings(snap, confirmed_at_poll_start)
|
|
1033
1046
|
if not snap["settings"]["pet_enabled"]:
|
|
1034
1047
|
return self._bye()
|
|
@@ -1375,12 +1388,7 @@ class PetApp:
|
|
|
1375
1388
|
|
|
1376
1389
|
def _open_ui(self):
|
|
1377
1390
|
try:
|
|
1378
|
-
|
|
1379
|
-
# 已开过就不新开:Windows 上先找现有 CodeBee 浏览器窗口置前,
|
|
1380
|
-
# 找不到(没开/标签不在前台)才回退 webbrowser.open 新开。
|
|
1381
|
-
if os.name == "nt" and _focus_codebee_window(self.root.winfo_id()):
|
|
1382
|
-
return
|
|
1383
|
-
webbrowser.open(url)
|
|
1391
|
+
openPetWindow(self.port, self.root.winfo_id())
|
|
1384
1392
|
except Exception:
|
|
1385
1393
|
pass
|
|
1386
1394
|
|
package/app/ui/app.js
CHANGED
|
@@ -8513,16 +8513,29 @@ function ztHarvestProfiles() {
|
|
|
8513
8513
|
});
|
|
8514
8514
|
}
|
|
8515
8515
|
|
|
8516
|
-
function ztProfAdd() {
|
|
8516
|
+
async function ztProfAdd() {
|
|
8517
8517
|
S.ztProfiles = ztHarvestProfiles();
|
|
8518
|
-
|
|
8518
|
+
// 新档案必须来自禅道产品清单。先拉清单,再自动选第一个尚未配置的产品,
|
|
8519
|
+
// 避免生成 product=0 的空卡,用户随后保存才看到“缺产品 ID”。
|
|
8520
|
+
if (!(S.ztProducts || []).length) {
|
|
8521
|
+
if (!($("zt-base-url") && $("zt-base-url").value.trim())) {
|
|
8522
|
+
toast(t("请先测试并保存禅道连接,再添加产品"), true);
|
|
8523
|
+
return;
|
|
8524
|
+
}
|
|
8525
|
+
await ztFetchCatalog(false);
|
|
8526
|
+
S.ztProfiles = ztHarvestProfiles();
|
|
8527
|
+
}
|
|
8528
|
+
const used = new Set(S.ztProfiles.map((p) => String(p.product || "")));
|
|
8529
|
+
const next = (S.ztProducts || []).find((p) => p.id && !used.has(String(p.id)));
|
|
8530
|
+
if (!next) {
|
|
8531
|
+
toast(t((S.ztProducts || []).length ? "禅道产品均已添加" : "未拉到可用产品,请先测试连接"), true);
|
|
8532
|
+
return;
|
|
8533
|
+
}
|
|
8534
|
+
S.ztProfiles.push({ product: Number(next.id), assigned_to: "", severity_cap: 0, our_sides: ["backend"],
|
|
8519
8535
|
repos: { backend: {}, frontend: {} }, repo_hints: { backend: "", frontend: "" },
|
|
8520
8536
|
owners: { backend: "", frontend: "", not_ours: "" }, module_routes: [] });
|
|
8521
8537
|
renderZentaoProfiles();
|
|
8522
|
-
|
|
8523
|
-
if (!(S.ztProducts || []).length && $("zt-base-url") && $("zt-base-url").value.trim()) {
|
|
8524
|
-
ztFetchCatalog(false);
|
|
8525
|
-
}
|
|
8538
|
+
ztFetchMods(S.ztProfiles.length - 1, true);
|
|
8526
8539
|
}
|
|
8527
8540
|
|
|
8528
8541
|
function ztProfDel(i) {
|
|
@@ -8672,6 +8685,13 @@ function ztFormPayload() {
|
|
|
8672
8685
|
|
|
8673
8686
|
async function saveZentao() {
|
|
8674
8687
|
const payload = ztFormPayload();
|
|
8688
|
+
if ((payload.product_profiles || []).some((p) => !p.product)) {
|
|
8689
|
+
toast(t("请选择禅道产品后再保存"), true);
|
|
8690
|
+
const empty = Array.from(document.querySelectorAll("#zt-profiles .zt-p-product"))
|
|
8691
|
+
.find((el) => !parseInt(el.value || "", 10));
|
|
8692
|
+
if (empty) empty.focus();
|
|
8693
|
+
return;
|
|
8694
|
+
}
|
|
8675
8695
|
try {
|
|
8676
8696
|
await api("/api/zentao/config", { method: "POST", body: JSON.stringify(payload) });
|
|
8677
8697
|
} catch (e) { toast(t("保存失败:") + e.message, true); return; }
|
|
@@ -10601,6 +10621,11 @@ function collapseDrawerIfMobile() {
|
|
|
10601
10621
|
function switchTab(name) {
|
|
10602
10622
|
if (name === "__phone") { openPhoneConnect(); return; } // 手机连接是弹框,不切页
|
|
10603
10623
|
if (name === "__guide") { welcomeOpen(); return; } // 帮助中心是弹层,不切页(设置导航「软件」组)
|
|
10624
|
+
if (name === "__wxdigest") { // 群聊汇报沿用全局蜜蜂面板
|
|
10625
|
+
beeRefresh().finally(() => beeToggle(true));
|
|
10626
|
+
collapseDrawerIfMobile();
|
|
10627
|
+
return;
|
|
10628
|
+
}
|
|
10604
10629
|
// 导航收进「设置」:进设置后左栏整体换成设置导航,内容铺满
|
|
10605
10630
|
S.tab = name;
|
|
10606
10631
|
if (SET_TABS.has(name)) localStorage.setItem("orch.setTab", name);
|
|
@@ -11314,7 +11339,7 @@ function bindCmdK() {
|
|
|
11314
11339
|
|
|
11315
11340
|
/* ------------------------------------------------ 群摘要蜜蜂坞(core/wxdigest.py)
|
|
11316
11341
|
* 右下角悬浮蜜蜂:未读摘要徽章+气泡;面板看各群摘要、配置监控文件夹。
|
|
11317
|
-
* 桌面蜜蜂 pet.py
|
|
11342
|
+
* 桌面蜜蜂 pet.py 的网页侧搭档;入口始终可见,未配置时也能直接打开设置。
|
|
11318
11343
|
* 60s 轮询 /api/wxdigest;打开面板即 POST seen 清零未读。 */
|
|
11319
11344
|
const Bee = { view: null, lastUnseen: 0, bubT: 0, selGroups: null };
|
|
11320
11345
|
|
|
@@ -11325,9 +11350,7 @@ async function beeRefresh() {
|
|
|
11325
11350
|
let v;
|
|
11326
11351
|
try { v = await api("/api/wxdigest"); } catch (e) { return; } // 静默:不打扰主流程
|
|
11327
11352
|
Bee.view = v;
|
|
11328
|
-
|
|
11329
|
-
(v.digests && v.digests.length) || v.unseen > 0;
|
|
11330
|
-
beeShow($("bee-dock"), on);
|
|
11353
|
+
beeShow($("bee-dock"), true);
|
|
11331
11354
|
beeShow($("bee-badge"), v.unseen > 0);
|
|
11332
11355
|
if (v.unseen > 0) $("bee-badge").textContent = v.unseen > 99 ? "99+" : String(v.unseen);
|
|
11333
11356
|
if (v.unseen > Bee.lastUnseen && !beePanelOpen()) { // 新摘要到:气泡 6s
|
package/app/ui/i18n.js
CHANGED
|
@@ -2005,6 +2005,10 @@
|
|
|
2005
2005
|
"还没有认领过 Bug——配置好连接与产品档案后点「立即扫描」。": "No bugs claimed yet — set up connection and product profiles, then click \"Scan Now\".",
|
|
2006
2006
|
"还没有产品档案——点「+ 添加产品」,填产品 ID 与两端仓库。": "No product profiles yet — click \"+ Add product\" and fill the product ID and repos.",
|
|
2007
2007
|
"还没有产品档案——先测试连接,再从禅道选择产品,无需自己查 ID。": "No product profiles yet — test the connection, then select a product from ZenTao; no ID lookup needed.",
|
|
2008
|
+
"请先测试并保存禅道连接,再添加产品": "Test and save the ZenTao connection before adding a product.",
|
|
2009
|
+
"禅道产品均已添加": "All ZenTao products have already been added.",
|
|
2010
|
+
"未拉到可用产品,请先测试连接": "No available products were found. Test the connection first.",
|
|
2011
|
+
"请选择禅道产品后再保存": "Select a ZenTao product before saving.",
|
|
2008
2012
|
"先填产品 ID 再拉模块清单": "Fill the product ID before fetching modules",
|
|
2009
2013
|
"先选产品再拉模块清单": "Pick a product before fetching modules",
|
|
2010
2014
|
"拉取失败": "Fetch failed",
|
|
@@ -2200,6 +2204,7 @@
|
|
|
2200
2204
|
|
|
2201
2205
|
// —— 群摘要蜜蜂坞(wxdigest)——
|
|
2202
2206
|
"群摘要": "Group Digests",
|
|
2207
|
+
"微信群聊汇报": "WeChat Group Reports",
|
|
2203
2208
|
"启用定时扫描": "Enable scheduled scanning",
|
|
2204
2209
|
"监控文件夹": "Watched folder",
|
|
2205
2210
|
"间隔(分钟)": "Interval (min)",
|
package/app/ui/index.html
CHANGED
|
@@ -178,6 +178,7 @@
|
|
|
178
178
|
<button class="set-item active" data-sub="tasks" data-i18n="任务"><svg class="ico" aria-hidden="true"><use href="#i-tasks"></use></svg>任务</button>
|
|
179
179
|
<button class="set-item" data-sub="runs" data-i18n="运行记录"><svg class="ico" aria-hidden="true"><use href="#i-history"></use></svg>运行记录</button>
|
|
180
180
|
<button class="set-item" data-sub="automation" data-i18n="自动化"><svg class="ico" aria-hidden="true"><use href="#i-calendar-days"></use></svg>自动化</button>
|
|
181
|
+
<button class="set-item" data-sub="__wxdigest" data-i18n="微信群聊汇报"><svg class="ico" aria-hidden="true"><use href="#i-bee"></use></svg>微信群聊汇报</button>
|
|
181
182
|
<button class="set-item" data-sub="zentao" data-i18n="禅道"><svg class="ico" aria-hidden="true"><use href="#i-search"></use></svg>禅道</button>
|
|
182
183
|
<button class="set-item" data-sub="usage" data-i18n="用量统计"><svg class="ico" aria-hidden="true"><use href="#i-chart"></use></svg>用量统计</button>
|
|
183
184
|
<div data-i18n="配置" class="set-group">配置</div>
|
|
@@ -557,12 +558,12 @@
|
|
|
557
558
|
<label data-i18n="密码">密码</label><input id="zt-password" type="password" placeholder="" autocomplete="new-password">
|
|
558
559
|
</div>
|
|
559
560
|
<h3 data-i18n="自动化" class="sec-title">自动化</h3>
|
|
560
|
-
<div class="zt-grid zt-grid-toggles">
|
|
561
|
+
<div class="zt-grid zt-grid-toggles zt-auto-options">
|
|
561
562
|
<label class="toggle" title="模块路由没命中时,用 AI 读 Bug 描述判端(前端/后端/双端/非我方);关掉则未命中模块的 Bug 留人工" data-i18n-title="模块路由没命中时,用 AI 读 Bug 描述判端(前端/后端/双端/非我方);关掉则未命中模块的 Bug 留人工"><input id="zt-triageai" type="checkbox"><span data-i18n="AI 排查兜底">AI 排查兜底</span></label>
|
|
562
563
|
<label class="toggle" title="修复任务评审通过后,自动到禅道 resolve(fixed)并回评论" data-i18n-title="修复任务评审通过后,自动到禅道 resolve(fixed)并回评论"><input id="zt-autoresolve" type="checkbox"><span data-i18n="自动 resolve Bug">自动 resolve Bug</span></label>
|
|
563
564
|
<label class="toggle" title="把任务分支自动合并回基线分支(需配置基线分支;合并失败不 resolve 不转派,留人工)" data-i18n-title="把任务分支自动合并回基线分支(需配置基线分支;合并失败不 resolve 不转派,留人工)"><input id="zt-automerge" type="checkbox"><span data-i18n="自动合并代码">自动合并代码</span></label>
|
|
564
565
|
<label class="toggle" title="按下面的间隔定时扫描;关掉后仍可手动「立即扫描」" data-i18n-title="按下面的间隔定时扫描;关掉后仍可手动「立即扫描」"><input id="zt-poll" type="checkbox"><span data-i18n="定时扫描">定时扫描</span></label>
|
|
565
|
-
<label data-i18n="扫描间隔(小时)">扫描间隔(小时)</
|
|
566
|
+
<label class="zt-interval-field"><span data-i18n="扫描间隔(小时)">扫描间隔(小时)</span><input id="zt-interval" type="number" min="1" max="168" step="1"></label>
|
|
566
567
|
</div>
|
|
567
568
|
<div class="ops"><button id="btn-zentao-save" class="primary small"><span data-i18n="保存配置">保存配置</span></button></div>
|
|
568
569
|
<h3 data-i18n="产品档案" class="sec-title">产品档案</h3>
|
|
@@ -1107,10 +1108,10 @@
|
|
|
1107
1108
|
|
|
1108
1109
|
<!-- 群摘要蜜蜂坞(右下角悬浮蜜蜂):桌面蜜蜂 pet.py 的网页侧搭档。
|
|
1109
1110
|
未读摘要时徽章+气泡提醒,点开面板看各群摘要/配置监控文件夹;
|
|
1110
|
-
|
|
1111
|
-
<div id="bee-dock"
|
|
1111
|
+
未配置时也保持入口可见,避免功能存在却无处开启。后端 core/wxdigest.py。 -->
|
|
1112
|
+
<div id="bee-dock">
|
|
1112
1113
|
<div id="bee-bubble" class="hidden"></div>
|
|
1113
|
-
<button id="bee-fab" aria-label="
|
|
1114
|
+
<button id="bee-fab" aria-label="微信群聊汇报" data-i18n-aria="微信群聊汇报" data-i18n-title="微信群聊汇报" title="微信群聊汇报">
|
|
1114
1115
|
<svg class="ico ico-lg" aria-hidden="true"><use href="#i-bee"></use></svg>
|
|
1115
1116
|
<span id="bee-badge" class="hidden"></span>
|
|
1116
1117
|
</button>
|
package/app/ui/style.css
CHANGED
|
@@ -5521,3 +5521,34 @@ button.request-busy > * { opacity: .18; }
|
|
|
5521
5521
|
line-height: 1.6;
|
|
5522
5522
|
}
|
|
5523
5523
|
#su-info.empty { text-align: center; }
|
|
5524
|
+
|
|
5525
|
+
/* ---------------- 禅道·添加产品与自动化布局(追加于文件尾) ---------------- */
|
|
5526
|
+
.zt-prof .head .name {
|
|
5527
|
+
display: flex; flex: 1 1 280px; min-width: 0; align-items: center; gap: 8px;
|
|
5528
|
+
overflow: visible; white-space: normal;
|
|
5529
|
+
}
|
|
5530
|
+
.zt-prof .head .zt-p-product {
|
|
5531
|
+
width: min(360px, 100%); min-width: 180px; flex: 1 1 240px;
|
|
5532
|
+
}
|
|
5533
|
+
.zt-auto-options {
|
|
5534
|
+
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
|
5535
|
+
gap: 12px 18px; align-items: start;
|
|
5536
|
+
}
|
|
5537
|
+
@media (min-width: 901px) {
|
|
5538
|
+
.zt-auto-options { grid-template-columns: repeat(5, minmax(0, 1fr)); }
|
|
5539
|
+
}
|
|
5540
|
+
.zt-auto-options > .toggle {
|
|
5541
|
+
min-height: 38px; margin: 0; align-self: end;
|
|
5542
|
+
}
|
|
5543
|
+
.zt-auto-options .zt-interval-field {
|
|
5544
|
+
display: grid; grid-template-columns: 1fr; gap: 6px; min-width: 0;
|
|
5545
|
+
color: var(--muted); font-size: 12.5px;
|
|
5546
|
+
}
|
|
5547
|
+
.zt-auto-options .zt-interval-field input {
|
|
5548
|
+
width: 100%; max-width: none; box-sizing: border-box;
|
|
5549
|
+
}
|
|
5550
|
+
@media (max-width: 640px) {
|
|
5551
|
+
.zt-auto-options { grid-template-columns: 1fr 1fr; }
|
|
5552
|
+
.zt-auto-options .zt-interval-field { grid-column: 1 / -1; }
|
|
5553
|
+
.zt-prof .head .zt-p-product { min-width: 0; }
|
|
5554
|
+
}
|
package/package.json
CHANGED