codebee 0.1.27 → 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 +16 -0
- package/README.md +13 -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/manager.py +41 -32
- 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 +36 -10
- package/app/ui/i18n.js +6 -0
- package/app/ui/index.html +6 -5
- package/app/ui/style.css +44 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,22 @@ README 元数据带回,供老版本在「发现新版本」时展示新版更
|
|
|
6
6
|
|
|
7
7
|
## 未发布
|
|
8
8
|
|
|
9
|
+
## v0.1.29(2026-09-21)
|
|
10
|
+
|
|
11
|
+
### ✨ 新功能
|
|
12
|
+
|
|
13
|
+
- 升级后蜜蜂自动换新装:修复「升级后还是旧的丑蜜蜂」——服务重启后旧蜜蜂会自动让位,由新服务拉起带新形象的新蜜蜂(此前旧蜜蜂占着位置永不退场)。
|
|
14
|
+
|
|
15
|
+
### 🛠 问题修复
|
|
16
|
+
|
|
17
|
+
- 去掉蜜蜂底部的任务进度条(真机截图反馈):窗口收紧贴着蜂体,画面更干净。
|
|
18
|
+
|
|
19
|
+
## v0.1.28(2026-09-21)
|
|
20
|
+
|
|
21
|
+
### 🛠 问题修复
|
|
22
|
+
|
|
23
|
+
- 「关于与更新」页的版本信息与更新说明改为左对齐阅读,不再整块居中(加载占位的居中样式误留在了正式内容上)。
|
|
24
|
+
|
|
9
25
|
## v0.1.27(2026-09-21)
|
|
10
26
|
|
|
11
27
|
### ✨ 新功能
|
package/README.md
CHANGED
|
@@ -23,6 +23,19 @@ Kimi Code、MiMo Code、Grok Build、Pi、DeepSeek Harness……),提供
|
|
|
23
23
|
<!-- relnotes:start -->
|
|
24
24
|
### ✨ 新功能 · New
|
|
25
25
|
|
|
26
|
+
- 升级后蜜蜂自动换新装:服务重启后旧蜜蜂自动让位,新蜜蜂带新形象上线。
|
|
27
|
+
- After upgrading, the desktop bee swaps itself for the new look: the old bee steps aside when the service restarts.
|
|
28
|
+
|
|
29
|
+
### 🛠 问题修复 · Fixes
|
|
30
|
+
|
|
31
|
+
- 去掉蜜蜂底部的任务进度条,窗口更干净。
|
|
32
|
+
- Removed the task progress bar under the desktop bee for a cleaner look.
|
|
33
|
+
|
|
34
|
+
- 「关于与更新」页的版本信息与更新说明改为左对齐阅读,不再整块居中。
|
|
35
|
+
- The version info and release notes on the About page are now left-aligned for easier reading.
|
|
36
|
+
|
|
37
|
+
### ✨ 新功能 · New
|
|
38
|
+
|
|
26
39
|
- 连载任务防穿帮:新增「资源账本」——每章评审自动登记新出现的道具、伤情、承诺与伏笔,写下一章前自动注入提醒核对。
|
|
27
40
|
- Serial tasks now keep a resource ledger: props, injuries, promises, and plot threads are tracked per chapter and injected before writing the next one.
|
|
28
41
|
|
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/manager.py
CHANGED
|
@@ -31,18 +31,18 @@ _LOCK = threading.RLock()
|
|
|
31
31
|
_STATE = {"detected": {}, "versions": {}, "detect_ts": 0.0, "detect_ev": None}
|
|
32
32
|
|
|
33
33
|
# 能自动写入默认模型的 config.format(其余格式只能手动编辑)
|
|
34
|
-
_WRITABLE_FORMATS = ("toml-line", "toml-section", "json", "json-path", "jsonc",
|
|
35
|
-
"yaml-line")
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
def _read_text(path, preserve_newlines=False):
|
|
39
|
-
"""读取文本并及时关闭句柄;改写配置前可保留原始换行符。"""
|
|
40
|
-
newline = "" if preserve_newlines else None
|
|
41
|
-
with open(path, encoding="utf-8", errors="replace", newline=newline) as fh:
|
|
42
|
-
return fh.read()
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
def _expand(p):
|
|
34
|
+
_WRITABLE_FORMATS = ("toml-line", "toml-section", "json", "json-path", "jsonc",
|
|
35
|
+
"yaml-line")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _read_text(path, preserve_newlines=False):
|
|
39
|
+
"""读取文本并及时关闭句柄;改写配置前可保留原始换行符。"""
|
|
40
|
+
newline = "" if preserve_newlines else None
|
|
41
|
+
with open(path, encoding="utf-8", errors="replace", newline=newline) as fh:
|
|
42
|
+
return fh.read()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _expand(p):
|
|
46
46
|
return os.path.abspath(os.path.expanduser(os.path.expandvars(p)))
|
|
47
47
|
|
|
48
48
|
|
|
@@ -482,7 +482,7 @@ def read_model(entry):
|
|
|
482
482
|
if not path or not os.path.isfile(path) or not cfg.get("format"):
|
|
483
483
|
return None
|
|
484
484
|
try:
|
|
485
|
-
text = _read_text(path)
|
|
485
|
+
text = _read_text(path)
|
|
486
486
|
except Exception:
|
|
487
487
|
return None
|
|
488
488
|
if cfg["format"] == "toml-line":
|
|
@@ -554,7 +554,7 @@ def write_model(entry, model):
|
|
|
554
554
|
# mimo(mimocode.jsonc)有注释,整体重解析会丢注释——复用
|
|
555
555
|
# _jsonc_set 做就地片段改写(只动目标键,其余原样保留)
|
|
556
556
|
keys = (cfg.get("model_key") or "model").split(".")
|
|
557
|
-
text = _read_text(path, preserve_newlines=True)
|
|
557
|
+
text = _read_text(path, preserve_newlines=True)
|
|
558
558
|
new_text, ok = _jsonc_set(text, tuple(keys),
|
|
559
559
|
json.dumps(model, ensure_ascii=False))
|
|
560
560
|
if not ok:
|
|
@@ -562,7 +562,7 @@ def write_model(entry, model):
|
|
|
562
562
|
"error": "jsonc 结构异常,未能就地写入 %s(已避免覆盖)" % path}
|
|
563
563
|
Path(path).write_bytes(new_text.encode("utf-8"))
|
|
564
564
|
elif fmt == "toml-line":
|
|
565
|
-
text = _read_text(path)
|
|
565
|
+
text = _read_text(path)
|
|
566
566
|
new_line = 'model = "%s"' % model
|
|
567
567
|
if re.search(r'(?m)^\s*model\s*=\s*"[^"]*"', text):
|
|
568
568
|
text = re.sub(r'(?m)^\s*model\s*=\s*"[^"]*"', new_line, text)
|
|
@@ -570,19 +570,19 @@ def write_model(entry, model):
|
|
|
570
570
|
text = text.rstrip("\n") + "\n" + new_line + "\n"
|
|
571
571
|
Path(path).write_bytes(text.encode("utf-8"))
|
|
572
572
|
elif fmt == "toml-section":
|
|
573
|
-
text = _read_text(path, preserve_newlines=True)
|
|
573
|
+
text = _read_text(path, preserve_newlines=True)
|
|
574
574
|
text = _toml_write_value(text, *_dotted_key(cfg), value=model)
|
|
575
575
|
Path(path).write_bytes(text.encode("utf-8"))
|
|
576
576
|
elif fmt == "yaml-line":
|
|
577
577
|
# newline="" 关掉通用换行转换:文本层面看不出 \r\n 就会被静默改写成 LF,
|
|
578
578
|
# 用户的 Windows 配置不该因为写个模型名而整篇换行符被替换
|
|
579
|
-
text = _read_text(path, preserve_newlines=True)
|
|
579
|
+
text = _read_text(path, preserve_newlines=True)
|
|
580
580
|
text = _yaml_write_value(text, *_yaml_model_path(cfg), value=model)
|
|
581
581
|
Path(path).write_bytes(text.encode("utf-8"))
|
|
582
582
|
elif fmt == "json-path":
|
|
583
583
|
# openclaw(agents.defaults.model.primary):默认模型藏在嵌套对象里,
|
|
584
584
|
# 且 openclaw 对未知顶层键直接拒绝启动——绝不能写顶层 "model"
|
|
585
|
-
text = _read_text(path, preserve_newlines=True)
|
|
585
|
+
text = _read_text(path, preserve_newlines=True)
|
|
586
586
|
try:
|
|
587
587
|
data = json.loads(text) if text.strip() else {}
|
|
588
588
|
except Exception:
|
|
@@ -604,7 +604,7 @@ def write_model(entry, model):
|
|
|
604
604
|
json.dumps(data, ensure_ascii=False, indent=2).encode("utf-8"))
|
|
605
605
|
else:
|
|
606
606
|
try:
|
|
607
|
-
data = json.loads(_read_text(path))
|
|
607
|
+
data = json.loads(_read_text(path))
|
|
608
608
|
except FileNotFoundError:
|
|
609
609
|
return {"ok": False,
|
|
610
610
|
"error": "配置文件尚未生成(%s 首次运行后才有),暂无法写入" % path}
|
|
@@ -662,7 +662,7 @@ def _launch_log_path(entry):
|
|
|
662
662
|
def _best_url(log_path, port):
|
|
663
663
|
"""从启动日志提取该端口的信任 URL(含 token 优先)。无日志/未匹配返回 None。"""
|
|
664
664
|
try:
|
|
665
|
-
text = _read_text(str(log_path))
|
|
665
|
+
text = _read_text(str(log_path))
|
|
666
666
|
except Exception:
|
|
667
667
|
return None
|
|
668
668
|
urls = re.findall(r"https?://[^\s\"'<>]+", text)
|
|
@@ -762,7 +762,7 @@ def _sync_dsh_settings(entry, model, base_url):
|
|
|
762
762
|
return "dsh 配置路径越出用户主目录,已拒绝"
|
|
763
763
|
try:
|
|
764
764
|
if os.path.isfile(path):
|
|
765
|
-
text = _read_text(path, preserve_newlines=True)
|
|
765
|
+
text = _read_text(path, preserve_newlines=True)
|
|
766
766
|
else:
|
|
767
767
|
Path(path).parent.mkdir(parents=True, exist_ok=True)
|
|
768
768
|
text = ""
|
|
@@ -785,7 +785,7 @@ def _dsh_selfcheck_model(entry):
|
|
|
785
785
|
if not path or not os.path.isfile(path):
|
|
786
786
|
return None, ""
|
|
787
787
|
try:
|
|
788
|
-
text = _read_text(path, preserve_newlines=True)
|
|
788
|
+
text = _read_text(path, preserve_newlines=True)
|
|
789
789
|
except Exception:
|
|
790
790
|
return None, ""
|
|
791
791
|
cur = _yaml_read_value(text, "agent-default-model", "model")
|
|
@@ -809,7 +809,7 @@ def _dsh_key_present():
|
|
|
809
809
|
return True
|
|
810
810
|
try:
|
|
811
811
|
envfile = os.path.join(os.path.expanduser("~"), ".dsh", ".env")
|
|
812
|
-
return "DEEPSEEK_API_KEY" in _read_text(envfile)
|
|
812
|
+
return "DEEPSEEK_API_KEY" in _read_text(envfile)
|
|
813
813
|
except Exception:
|
|
814
814
|
return False
|
|
815
815
|
|
|
@@ -915,7 +915,7 @@ def _sync_codex_settings(entry, model, cp):
|
|
|
915
915
|
("wire_api", q(cp.get("wire_api", "responses")))]
|
|
916
916
|
try:
|
|
917
917
|
if os.path.isfile(path):
|
|
918
|
-
text = _read_text(path, preserve_newlines=True)
|
|
918
|
+
text = _read_text(path, preserve_newlines=True)
|
|
919
919
|
else:
|
|
920
920
|
Path(path).parent.mkdir(parents=True, exist_ok=True)
|
|
921
921
|
text = ""
|
|
@@ -1122,7 +1122,7 @@ def _sync_settings_env(path, updates, remove_keys=()):
|
|
|
1122
1122
|
不覆盖;改动前 .bak。返回错误串或 None。path 必须已过主目录围栏校验。"""
|
|
1123
1123
|
try:
|
|
1124
1124
|
if os.path.isfile(path):
|
|
1125
|
-
text = _read_text(path)
|
|
1125
|
+
text = _read_text(path)
|
|
1126
1126
|
try:
|
|
1127
1127
|
data = json.loads(text)
|
|
1128
1128
|
except Exception:
|
|
@@ -1242,7 +1242,7 @@ def _sync_opencode_settings(entry, model, prov):
|
|
|
1242
1242
|
errs.append("路径越出主目录:%s" % path)
|
|
1243
1243
|
continue
|
|
1244
1244
|
if os.path.isfile(path):
|
|
1245
|
-
text = _read_text(path, preserve_newlines=True)
|
|
1245
|
+
text = _read_text(path, preserve_newlines=True)
|
|
1246
1246
|
else:
|
|
1247
1247
|
Path(path).parent.mkdir(parents=True, exist_ok=True)
|
|
1248
1248
|
text = ""
|
|
@@ -1333,7 +1333,7 @@ def _sync_kimi_settings(entry, model, prov):
|
|
|
1333
1333
|
Path(path).parent.mkdir(parents=True, exist_ok=True)
|
|
1334
1334
|
text = ""
|
|
1335
1335
|
if os.path.isfile(path):
|
|
1336
|
-
text = _read_text(path, preserve_newlines=True)
|
|
1336
|
+
text = _read_text(path, preserve_newlines=True)
|
|
1337
1337
|
shutil.copyfile(path, path + ".bak")
|
|
1338
1338
|
# 移除旧托管块与旧顶层键(防重复/防键落进别的表)
|
|
1339
1339
|
text = _re.sub(r"# >>> CodeBee managed.*?# <<< CodeBee managed <<<\n?",
|
|
@@ -1410,7 +1410,7 @@ def _sync_agent_injection(entry, binding):
|
|
|
1410
1410
|
if entry["id"] in _NO_CHANNEL_HINT:
|
|
1411
1411
|
if binding.get("env"):
|
|
1412
1412
|
return "已按绑定注入 env,但该 CLI 未必认 CodeBee 的凭据通道,打开后若要求登录请在其界面内登录"
|
|
1413
|
-
return "未指定可用供应商:打开后使用 CLI 自带登录;需要固定注入请到「模型调度(可选)」页指定"
|
|
1413
|
+
return "未指定可用供应商:打开后使用 CLI 自带登录;需要固定注入请到「模型调度(可选)」页指定"
|
|
1414
1414
|
return None
|
|
1415
1415
|
|
|
1416
1416
|
|
|
@@ -1418,8 +1418,9 @@ def _sync_launch_model(entry, binding):
|
|
|
1418
1418
|
"""打开前把「CLI 绑定」页选中的模型落到该 CLI 自己的配置文件——保证
|
|
1419
1419
|
交互/网页版启动即选中绑定模型(绑定页是运行时模型唯一真源,目录页的
|
|
1420
1420
|
「默认模型」只是手动快照,会滞后)。dsh 额外同步端点与 models 目录;
|
|
1421
|
-
codex
|
|
1422
|
-
|
|
1421
|
+
codex 已改为不落盘(2026-09-21 起:全局 model_provider 归 CC Switch/
|
|
1422
|
+
用户管理,编排任务的绑定走 runner 的 -c 一次性注入)。返回给用户
|
|
1423
|
+
看的同步笔记列表。"""
|
|
1423
1424
|
notes = []
|
|
1424
1425
|
model = (binding.get("model") or "").strip()
|
|
1425
1426
|
prov = binding.get("provider") or {}
|
|
@@ -1427,6 +1428,14 @@ def _sync_launch_model(entry, binding):
|
|
|
1427
1428
|
is_dsh = entry["id"] in ("deepseek-harness", "dsh")
|
|
1428
1429
|
is_codex = entry["id"] in ("codex-cli", "codex")
|
|
1429
1430
|
cp = binding.get("codex_provider")
|
|
1431
|
+
if is_codex:
|
|
1432
|
+
# codex 全局配置不落盘(2026-09-21 用户决策):全局 model_provider 交还
|
|
1433
|
+
# CC Switch/用户手动管理,一键打开不再顶掉外部选择。编排任务不受影响
|
|
1434
|
+
# ——runner 的 -c 一次性注入仍带绑定供应商;交互 TUI 打开即用外部当前
|
|
1435
|
+
# 配置,想用绑定供应商起编排任务即可。
|
|
1436
|
+
notes.append("codex 配置保持外部管理(绑定 %s 仅用于编排 -c 注入,未写 config.toml)"
|
|
1437
|
+
% (cp.get("name") or "orch"))
|
|
1438
|
+
return notes
|
|
1430
1439
|
if model and fmt in _WRITABLE_FORMATS:
|
|
1431
1440
|
w = write_model(entry, model)
|
|
1432
1441
|
notes.append("模型已同步为 %s" % w["model"] if w.get("ok")
|
|
@@ -1530,7 +1539,7 @@ def launch(entry, open_browser=True):
|
|
|
1530
1539
|
notes = _sync_launch_model(entry, binding)
|
|
1531
1540
|
if is_dsh and not (binding.get("env") or {}) and not _dsh_key_present():
|
|
1532
1541
|
notes.append("未发现 dsh 密钥:打开后可能需在 dsh 内登录配置;"
|
|
1533
|
-
"要打开即用,请到「模型调度(可选)」页给 DeepSeek Harness 指定 openai 协议供应商")
|
|
1542
|
+
"要打开即用,请到「模型调度(可选)」页给 DeepSeek Harness 指定 openai 协议供应商")
|
|
1534
1543
|
name = entry.get("name", entry["id"])
|
|
1535
1544
|
kind = (launch.get("kind") or "console").lower()
|
|
1536
1545
|
extra = (";".join(notes)) if notes else ""
|
|
@@ -1637,7 +1646,7 @@ def _open_when_ready(port, url, log_path, timeout=30):
|
|
|
1637
1646
|
# ---------------------------------------------------------------- 安装/升级
|
|
1638
1647
|
|
|
1639
1648
|
def run_mgmt_command(entry, op, cancel_event=None, log_path=None):
|
|
1640
|
-
"""执行 install/upgrade/uninstall 命令(由任务执行器异步运行,日志实时落盘)。"""
|
|
1649
|
+
"""执行 install/upgrade/uninstall 命令(由任务执行器异步运行,日志实时落盘)。"""
|
|
1641
1650
|
if op == "uninstall":
|
|
1642
1651
|
cmd = catalog.uninstall_command(entry)
|
|
1643
1652
|
if not cmd:
|
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; }
|
|
@@ -9459,6 +9479,9 @@ function renderSu() {
|
|
|
9459
9479
|
const info = $("su-info");
|
|
9460
9480
|
if (!info) return;
|
|
9461
9481
|
if (!SU) { info.textContent = t("无法获取版本信息(服务未连接)"); return; }
|
|
9482
|
+
// 真实内容就绪后摘掉「空状态」皮肤:.empty 是居中+虚线框的加载占位样式,
|
|
9483
|
+
// 版本信息与更新说明都应该左对齐阅读(用户反馈「能不能左对齐」)
|
|
9484
|
+
info.classList.remove("empty");
|
|
9462
9485
|
const modeTxt = t(SU_MODE_TXT[SU.mode] || "未知安装方式");
|
|
9463
9486
|
const cur = SU.current ? "v" + SU.current : t("(未同步版本号)");
|
|
9464
9487
|
let html = "<p class=\"hint\">" + t("当前版本") + t(":") + "<b>" + cur + "</b>" + t(" · ") + t("安装方式") + t(":") + modeTxt + "</p>";
|
|
@@ -10598,6 +10621,11 @@ function collapseDrawerIfMobile() {
|
|
|
10598
10621
|
function switchTab(name) {
|
|
10599
10622
|
if (name === "__phone") { openPhoneConnect(); return; } // 手机连接是弹框,不切页
|
|
10600
10623
|
if (name === "__guide") { welcomeOpen(); return; } // 帮助中心是弹层,不切页(设置导航「软件」组)
|
|
10624
|
+
if (name === "__wxdigest") { // 群聊汇报沿用全局蜜蜂面板
|
|
10625
|
+
beeRefresh().finally(() => beeToggle(true));
|
|
10626
|
+
collapseDrawerIfMobile();
|
|
10627
|
+
return;
|
|
10628
|
+
}
|
|
10601
10629
|
// 导航收进「设置」:进设置后左栏整体换成设置导航,内容铺满
|
|
10602
10630
|
S.tab = name;
|
|
10603
10631
|
if (SET_TABS.has(name)) localStorage.setItem("orch.setTab", name);
|
|
@@ -11311,7 +11339,7 @@ function bindCmdK() {
|
|
|
11311
11339
|
|
|
11312
11340
|
/* ------------------------------------------------ 群摘要蜜蜂坞(core/wxdigest.py)
|
|
11313
11341
|
* 右下角悬浮蜜蜂:未读摘要徽章+气泡;面板看各群摘要、配置监控文件夹。
|
|
11314
|
-
* 桌面蜜蜂 pet.py
|
|
11342
|
+
* 桌面蜜蜂 pet.py 的网页侧搭档;入口始终可见,未配置时也能直接打开设置。
|
|
11315
11343
|
* 60s 轮询 /api/wxdigest;打开面板即 POST seen 清零未读。 */
|
|
11316
11344
|
const Bee = { view: null, lastUnseen: 0, bubT: 0, selGroups: null };
|
|
11317
11345
|
|
|
@@ -11322,9 +11350,7 @@ async function beeRefresh() {
|
|
|
11322
11350
|
let v;
|
|
11323
11351
|
try { v = await api("/api/wxdigest"); } catch (e) { return; } // 静默:不打扰主流程
|
|
11324
11352
|
Bee.view = v;
|
|
11325
|
-
|
|
11326
|
-
(v.digests && v.digests.length) || v.unseen > 0;
|
|
11327
|
-
beeShow($("bee-dock"), on);
|
|
11353
|
+
beeShow($("bee-dock"), true);
|
|
11328
11354
|
beeShow($("bee-badge"), v.unseen > 0);
|
|
11329
11355
|
if (v.unseen > 0) $("bee-badge").textContent = v.unseen > 99 ? "99+" : String(v.unseen);
|
|
11330
11356
|
if (v.unseen > Bee.lastUnseen && !beePanelOpen()) { // 新摘要到:气泡 6s
|
package/app/ui/i18n.js
CHANGED
|
@@ -1963,6 +1963,7 @@
|
|
|
1963
1963
|
"产品档案": "Product Profiles",
|
|
1964
1964
|
"+ 添加产品": "+ Add product",
|
|
1965
1965
|
"+ 从禅道添加产品": "+ Add from ZenTao",
|
|
1966
|
+
"测试并保存连接": "Test & save connection",
|
|
1966
1967
|
"刷新产品列表": "Refresh products",
|
|
1967
1968
|
"重新从禅道拉取产品与账号": "Reload products and accounts from ZenTao",
|
|
1968
1969
|
"先测试连接,再点「从禅道添加产品」直接选择产品;无需自己查 ID。每个产品只需配置我方仓库,负责人和模块路由均可稍后补充。": "Test the connection, then choose Add from ZenTao — no ID lookup needed. Configure only your repository first; owners and module routes can be added later.",
|
|
@@ -2004,6 +2005,10 @@
|
|
|
2004
2005
|
"还没有认领过 Bug——配置好连接与产品档案后点「立即扫描」。": "No bugs claimed yet — set up connection and product profiles, then click \"Scan Now\".",
|
|
2005
2006
|
"还没有产品档案——点「+ 添加产品」,填产品 ID 与两端仓库。": "No product profiles yet — click \"+ Add product\" and fill the product ID and repos.",
|
|
2006
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.",
|
|
2007
2012
|
"先填产品 ID 再拉模块清单": "Fill the product ID before fetching modules",
|
|
2008
2013
|
"先选产品再拉模块清单": "Pick a product before fetching modules",
|
|
2009
2014
|
"拉取失败": "Fetch failed",
|
|
@@ -2199,6 +2204,7 @@
|
|
|
2199
2204
|
|
|
2200
2205
|
// —— 群摘要蜜蜂坞(wxdigest)——
|
|
2201
2206
|
"群摘要": "Group Digests",
|
|
2207
|
+
"微信群聊汇报": "WeChat Group Reports",
|
|
2202
2208
|
"启用定时扫描": "Enable scheduled scanning",
|
|
2203
2209
|
"监控文件夹": "Watched folder",
|
|
2204
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
|
@@ -5508,3 +5508,47 @@ button.request-busy > * { opacity: .18; }
|
|
|
5508
5508
|
@media (prefers-reduced-motion: reduce) {
|
|
5509
5509
|
.op-pill.running .ico, .op-row.running .op-state { animation: none; }
|
|
5510
5510
|
}
|
|
5511
|
+
|
|
5512
|
+
/* 关于与更新:版本信息面板常态样式(加载中才挂 .empty 居中虚线皮肤;
|
|
5513
|
+
renderSu 填充内容后移除 empty,改用这里的左对齐阅读样式——用户反馈) */
|
|
5514
|
+
#su-info {
|
|
5515
|
+
padding: 12px 14px;
|
|
5516
|
+
border: 1px solid var(--border, rgba(128, 128, 128, .25));
|
|
5517
|
+
border-radius: 8px;
|
|
5518
|
+
background: var(--bg-soft, rgba(128, 128, 128, .06));
|
|
5519
|
+
text-align: left;
|
|
5520
|
+
font-size: 13px;
|
|
5521
|
+
line-height: 1.6;
|
|
5522
|
+
}
|
|
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