codebee 0.1.69 → 0.1.71

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.
Files changed (133) hide show
  1. package/CHANGELOG.md +589 -543
  2. package/LICENSE +21 -21
  3. package/README.md +509 -508
  4. package/app/.playwright-cli/console-2026-09-23T08-26-03-635Z.log +88 -0
  5. package/app/.playwright-cli/page-2026-09-23T08-26-03-845Z.yml +126 -0
  6. package/app/.playwright-cli/page-2026-09-23T08-29-44-203Z.png +0 -0
  7. package/app/.playwright-cli/page-2026-09-23T08-35-42-046Z.yml +210 -0
  8. package/app/.playwright-cli/page-2026-09-23T08-36-52-858Z.png +0 -0
  9. package/app/.playwright-cli/page-2026-09-23T08-37-33-938Z.png +0 -0
  10. package/app/.playwright-cli/run-detail-after.png +0 -0
  11. package/app/browser-final-1440.png +0 -0
  12. package/app/browser-final-390.png +0 -0
  13. package/app/browser-google-fallback.png +0 -0
  14. package/app/browser-google-test.png +0 -0
  15. package/app/browser-workbench-1440.png +0 -0
  16. package/app/browser-workbench-390.png +0 -0
  17. package/app/browser-workspace-google.png +0 -0
  18. package/app/browser-workspace-smoke.png +0 -0
  19. package/app/browser-workspace.png +0 -0
  20. package/app/core/acceptance.py +100 -100
  21. package/app/core/aiflavor.py +197 -197
  22. package/app/core/artifacts.py +148 -148
  23. package/app/core/assets.py +110 -110
  24. package/app/core/attachments.py +623 -623
  25. package/app/core/audit.py +30 -0
  26. package/app/core/automation.py +107 -12
  27. package/app/core/backup.py +470 -470
  28. package/app/core/beekeeper.py +183 -183
  29. package/app/core/benchstore.py +95 -0
  30. package/app/core/board.py +209 -209
  31. package/app/core/bookmeta.py +935 -930
  32. package/app/core/bookmeta_catalog.py +180 -180
  33. package/app/core/branching.py +137 -137
  34. package/app/core/builtin_agent.py +1673 -1234
  35. package/app/core/capability.py +144 -144
  36. package/app/core/catalog.py +7 -1
  37. package/app/core/chaptersafe.py +32 -32
  38. package/app/core/cleanup.py +303 -303
  39. package/app/core/compaction.py +201 -201
  40. package/app/core/covergen.py +356 -356
  41. package/app/core/defectretro.py +43 -43
  42. package/app/core/diagnostics.py +114 -114
  43. package/app/core/dispatch.py +190 -173
  44. package/app/core/dispatch_log.py +128 -128
  45. package/app/core/env_scrub.py +85 -85
  46. package/app/core/error_codes.py +11 -2
  47. package/app/core/errorlog.py +137 -137
  48. package/app/core/evalbench.py +720 -0
  49. package/app/core/evaluation.py +174 -174
  50. package/app/core/failure_notify.py +290 -290
  51. package/app/core/flows.py +234 -9
  52. package/app/core/goal_service.py +167 -167
  53. package/app/core/health.py +374 -374
  54. package/app/core/history.py +38 -38
  55. package/app/core/hooks.py +162 -162
  56. package/app/core/jobs.py +22 -1
  57. package/app/core/manager.py +2143 -2122
  58. package/app/core/market_remote.py +10 -0
  59. package/app/core/mcp_client.py +287 -0
  60. package/app/core/mocks.py +64 -64
  61. package/app/core/modelhub.py +185 -2
  62. package/app/core/notify.py +201 -106
  63. package/app/core/novel_quality.py +189 -0
  64. package/app/core/operations.py +228 -228
  65. package/app/core/paihang.py +138 -138
  66. package/app/core/paths.py +4 -0
  67. package/app/core/pipeline.py +426 -28
  68. package/app/core/planner.py +243 -22
  69. package/app/core/plugins.py +525 -0
  70. package/app/core/portguard.py +111 -111
  71. package/app/core/preview.py +293 -293
  72. package/app/core/publish/__init__.py +7 -7
  73. package/app/core/publish/auto.py +367 -367
  74. package/app/core/publish/browser.py +655 -539
  75. package/app/core/publish/fanqie.py +165 -159
  76. package/app/core/publish/flow.py +589 -440
  77. package/app/core/publish/flows-fanqie-calibrated.json +336 -332
  78. package/app/core/publish/ledger.py +229 -203
  79. package/app/core/publish/manager.py +824 -692
  80. package/app/core/publish/qimao.py +146 -141
  81. package/app/core/publish/ws.py +139 -139
  82. package/app/core/redact.py +38 -38
  83. package/app/core/remotesync.py +121 -0
  84. package/app/core/repeat_guard.py +129 -129
  85. package/app/core/router.py +398 -398
  86. package/app/core/runner.py +2142 -2067
  87. package/app/core/selfupdate.py +384 -384
  88. package/app/core/session_log.py +161 -161
  89. package/app/core/sessions.py +312 -312
  90. package/app/core/settings.py +42 -4
  91. package/app/core/settings_schema.py +270 -262
  92. package/app/core/share_page.py +114 -114
  93. package/app/core/skill_scan.py +122 -122
  94. package/app/core/skillpacks/fanqie-novel.md +80 -80
  95. package/app/core/skillpacks/market/character-bible.md +66 -66
  96. package/app/core/skillpacks/market/code-risk-checklist.md +58 -58
  97. package/app/core/skillpacks/market/git-workflow.md +57 -57
  98. package/app/core/skillpacks/market/release-notes.md +72 -72
  99. package/app/core/skillpacks/market/weekly-report.md +71 -71
  100. package/app/core/skillpacks/market/worldview-consistency.md +70 -70
  101. package/app/core/step_runner.py +132 -132
  102. package/app/core/store.py +30 -8
  103. package/app/core/task_compile.py +199 -199
  104. package/app/core/telemetry.py +292 -292
  105. package/app/core/tlsctx.py +64 -64
  106. package/app/core/token_meter.py +147 -147
  107. package/app/core/tracing.py +144 -144
  108. package/app/core/upstream.py +41 -41
  109. package/app/core/usage.py +1035 -862
  110. package/app/core/volumes.py +389 -389
  111. package/app/core/wxdigest.py +858 -858
  112. package/app/core/zentao.py +2614 -2465
  113. package/app/current-browser.png +0 -0
  114. package/app/main.py +316 -17
  115. package/app/mcp_server.py +175 -0
  116. package/app/pet.py +83 -22
  117. package/app/pick_dialog.py +110 -110
  118. package/app/plugins/code-review-helper/.codex-plugin/plugin.json +18 -0
  119. package/app/plugins/code-review-helper/skills/review.md +16 -0
  120. package/app/ui/app.js +1391 -77
  121. package/app/ui/artdiff.js +74 -74
  122. package/app/ui/board.html +162 -162
  123. package/app/ui/board.js +305 -305
  124. package/app/ui/i18n.js +249 -7
  125. package/app/ui/icons/bee.svg +79 -79
  126. package/app/ui/index.html +277 -13
  127. package/app/ui/manifest.json +16 -16
  128. package/app/ui/qrcode.js +2297 -2297
  129. package/app/ui/style.css +845 -12
  130. package/app//344/275/234/345/223/201/344/277/241/346/201/257-/344/270/203/347/214/253.md +18 -0
  131. package/bin/tutti.js +147 -147
  132. package/package.json +47 -46
  133. package/tools/wx_reader.py +261 -261
Binary file
package/app/main.py CHANGED
@@ -34,6 +34,7 @@ if os.environ.get("TUTTI_ISOLATE_HOME", "").strip() == "1":
34
34
  from core import automation, board, catalog, flows, jobs, manager, market, market_remote, preview, registry, remote, settings, store
35
35
  from core import paths
36
36
  from core import health
37
+ from core import usage as usage_ledger
37
38
  import pick_dialog
38
39
  import pet
39
40
 
@@ -219,6 +220,19 @@ class Handler(BaseHTTPRequestHandler):
219
220
 
220
221
  # ------------------------------------------------------------ 路由
221
222
  def do_GET(self):
223
+ # 顶层兜底:任何路由裸抛以前=socketserver 直接掐断连接、零日志,
224
+ # 浏览器只见 "Failed to fetch"(2026-09-29 store 局部名遮蔽实案)。
225
+ # 现在统一落日志并回 500 JSON;对端已断开时无从回话,原样上抛。
226
+ try:
227
+ return self._route_get()
228
+ except (BrokenPipeError, ConnectionAbortedError, ConnectionResetError,
229
+ socket.timeout):
230
+ raise
231
+ except Exception:
232
+ log.exception("GET %s 处理异常", getattr(self, "path", "?"))
233
+ self._safe_500()
234
+
235
+ def _route_get(self):
222
236
  path = urlparse(self.path).path
223
237
  m = None
224
238
  # 静态页不设防(无敏感信息):远程裸地址打开时由前端令牌门引导输入
@@ -267,6 +281,10 @@ class Handler(BaseHTTPRequestHandler):
267
281
  return self._json(200, {"sources": modelhub.sources()})
268
282
  if path == "/api/flows":
269
283
  return self._json(200, {"flows": flows.list_flows()})
284
+ if path == "/api/flows/versions":
285
+ qs = parse_qs(urlparse(self.path).query)
286
+ fid = (qs.get("id") or [""])[0]
287
+ return self._json(200, {"versions": flows.flow_versions(fid)})
270
288
  if path == "/api/skills":
271
289
  from core import skills
272
290
  return self._json(200, skills.view())
@@ -288,6 +306,9 @@ class Handler(BaseHTTPRequestHandler):
288
306
  if path == "/api/orchestrator":
289
307
  from core import modelhub
290
308
  return self._json(200, {"orchestrator": modelhub.orchestrator_view()})
309
+ if path == "/api/evalbench":
310
+ from core import evalbench
311
+ return self._json(200, evalbench.state())
291
312
  if path == "/api/catalog":
292
313
  return self._json(200, {"catalog": manager.catalog_view(),
293
314
  "checking": manager.updates_checking()})
@@ -362,6 +383,19 @@ class Handler(BaseHTTPRequestHandler):
362
383
  "Content-Disposition":
363
384
  'attachment; filename="codebee-diag-%s.zip"'
364
385
  % time.strftime("%Y%m%d-%H%M%S")})
386
+ if path == "/api/usage/export":
387
+ # 台账明细导出 CSV(UTF-8 BOM,Excel 直开;行数封顶防炸)
388
+ from core import usage as _usage
389
+ try:
390
+ days = max(0, min(3650, int((parse_qs(urlparse(self.path).query)
391
+ .get("days") or ["30"])[0])))
392
+ except (TypeError, ValueError):
393
+ days = 30
394
+ csv_bytes = _usage.export_csv(days=days)
395
+ return self._send(200, csv_bytes, ctype="text/csv; charset=utf-8",
396
+ headers={"Content-Disposition":
397
+ 'attachment; filename="codebee-usage-%s.csv"'
398
+ % time.strftime("%Y%m%d-%H%M%S")})
365
399
  if path == "/api/diagnostics/issue-summary":
366
400
  # 一键反馈 Issue 的预填摘要(标题+正文,全程脱敏,用户亲手提交)
367
401
  from core import telemetry
@@ -514,8 +548,12 @@ class Handler(BaseHTTPRequestHandler):
514
548
  if m:
515
549
  from core.publish import manager as pub
516
550
  from core.publish import ledger as pub_ledger
517
- return self._json(200, {"history": pub.history(task_id=m.group(1), limit=50),
518
- "books": pub_ledger.load_books().get(m.group(1)) or {}})
551
+ tid = m.group(1)
552
+ books = pub_ledger.load_books().get(tid) or {}
553
+ return self._json(200, {"history": pub.history(task_id=tid, limit=50),
554
+ "books": books,
555
+ "published": {p: pub.published_local(tid, p)
556
+ for p in books}})
519
557
  m = re.match(r"^/api/publish/task/([^/]+)/pending$", path)
520
558
  if m:
521
559
  # 自动发布视图:待发清单统计 + 护栏状态 + 批量发布进度
@@ -642,6 +680,24 @@ class Handler(BaseHTTPRequestHandler):
642
680
  return self._json(404, {"error": "not found"})
643
681
 
644
682
  def do_POST(self):
683
+ # 同 do_GET:路由裸抛统一落日志并回 500,不再静默掐连接
684
+ try:
685
+ return self._route_post()
686
+ except (BrokenPipeError, ConnectionAbortedError, ConnectionResetError,
687
+ socket.timeout):
688
+ raise
689
+ except Exception:
690
+ log.exception("POST %s 处理异常", getattr(self, "path", "?"))
691
+ self._safe_500()
692
+
693
+ def _safe_500(self):
694
+ # 响应头可能已发出(流式路由半途抛),二次失败只能放弃
695
+ try:
696
+ self._json(500, {"error": "服务内部错误,详情见服务端日志"})
697
+ except Exception:
698
+ pass
699
+
700
+ def _route_post(self):
645
701
  path = urlparse(self.path).path
646
702
  m = None
647
703
  if not self._authed():
@@ -665,7 +721,8 @@ class Handler(BaseHTTPRequestHandler):
665
721
  # /api/hooks/run 用自己的令牌鉴权(外部脚本没有设备控制权握手)。
666
722
  if path in ("/api/health/op", "/api/models/provider-op", "/api/models/model-op",
667
723
  "/api/models/test-provider", "/api/models/test-model",
668
- "/api/models/probe-wire", "/api/models/key-op", "/api/hooks/run"):
724
+ "/api/models/probe-wire", "/api/models/key-op", "/api/hooks/run",
725
+ "/api/notify/test", "/api/models/set-price", "/api/models/model-caps"):
669
726
  pass # 落到下方各自路由
670
727
  else:
671
728
  deny = self._deny_control()
@@ -690,6 +747,50 @@ class Handler(BaseHTTPRequestHandler):
690
747
  return self._json(200, {"ok": ok, "output": out})
691
748
  if path == "/api/tasks/clarify":
692
749
  return self._api_task_clarify()
750
+ if path == "/api/notify/test":
751
+ # 设置页「发送测试」:向全部已配置推送通道发一条测试消息
752
+ from core import notify
753
+ res = notify.push_text_ex(
754
+ "🔔 CodeBee 测试推送\n收到这条消息说明推送通道连通正常(任务收尾会推同样格式的结果摘要)。")
755
+ if not res:
756
+ return self._json(400, {"error": "未配置任何推送通道(群 Webhook / Bark / ntfy / Server酱 / Telegram)"})
757
+ return self._json(200, {"ok": any(res.values()), "channels": res})
758
+ if path == "/api/evalbench/run":
759
+ # 评测基准台:样题 × 候选模型实跑 + 编排者裁判打分(后台线程)
760
+ from core import evalbench
761
+ body = self._body() or {}
762
+ info, err = evalbench.start(body.get("candidates"),
763
+ sample_ids=body.get("samples"))
764
+ if err:
765
+ return self._json(409 if "在跑" in err else 400, {"error": err})
766
+ return self._json(200, dict(ok=True, **info))
767
+ if path == "/api/evalbench/cancel":
768
+ from core import evalbench
769
+ return self._json(200, {"ok": evalbench.cancel()})
770
+ if path == "/api/evalbench/sample-op":
771
+ # 自定义样题增删:{op:add|delete, sample:{...}|sid}
772
+ from core import evalbench
773
+ body = self._body() or {}
774
+ res, err = evalbench.sample_op(body.get("op") or "",
775
+ sample=body.get("sample"),
776
+ sid=body.get("sid") or "")
777
+ if err:
778
+ return self._json(400, {"error": err})
779
+ return self._json(200, {"ok": True, "sample": res})
780
+ if path == "/api/evalbench/sample-export":
781
+ from core import evalbench
782
+ body = self._body() or {}
783
+ code, err = evalbench.export_sample_code(body.get("id") or "")
784
+ if err:
785
+ return self._json(400, {"error": err})
786
+ return self._json(200, {"ok": True, "code": code})
787
+ if path == "/api/evalbench/sample-import":
788
+ from core import evalbench
789
+ body = self._body() or {}
790
+ res, err = evalbench.import_sample_code(body.get("code"))
791
+ if err:
792
+ return self._json(400, {"error": err})
793
+ return self._json(200, dict(ok=True, **res))
693
794
  if path == "/api/hooks/run":
694
795
  return self._api_hook_run()
695
796
  if path == "/api/health/op":
@@ -811,7 +912,10 @@ class Handler(BaseHTTPRequestHandler):
811
912
  _t = store.get_task(m.group(1)) or {}
812
913
  return self._json(200, {"ok": True, "rev": _t.get("rev")})
813
914
  else:
814
- ok, err = store.delete_task(m.group(1))
915
+ # actor 记进删除审计(store 落台账);本地回环标 local 更可读
916
+ ip = self.client_address[0] if self.client_address else ""
917
+ ok, err = store.delete_task(
918
+ m.group(1), actor="local" if ip in ("127.0.0.1", "::1") else ip)
815
919
  return self._json(400, {"error": err}) if not ok else self._json(200, {"ok": True})
816
920
  m = re.match(r"^/api/tasks/([^/]+)/bible$", path)
817
921
  if m:
@@ -839,6 +943,39 @@ class Handler(BaseHTTPRequestHandler):
839
943
  m = re.match(r"^/api/publish/task/([^/]+)/publish-all$", path)
840
944
  if m:
841
945
  return self._api_publish_auto(m.group(1))
946
+ m = re.match(r"^/api/publish/task/([^/]+)/sync-published$", path)
947
+ if m:
948
+ # 已发章数校准:数章节管理页的实况写回登记(后台线程)。
949
+ # manual=true(用户点「校准」)允许 attach-or-launch;自动触发
950
+ # 只 attach 在跑实例,绝不因打开页面弹出新浏览器窗口。
951
+ from core.publish import manager as pub
952
+ from core.publish import ledger as pub_ledger
953
+ # 注意:这里绝不能再 `from core import store`——函数内 import 会把
954
+ # store 变成 do_POST 整个函数的局部名,同函数里其余路由(新建任务/
955
+ # 取消/升级全部等)一读 store 就是 UnboundLocalError,连接被静默
956
+ # 掐断,前端只见 "Failed to fetch"(0.1.70 实案,bisect 定位)。
957
+ # store 已在模块顶层导入,直接用。
958
+ tid = m.group(1)
959
+ if not store.get_task(tid):
960
+ return self._json(404, {"error": "任务不存在"})
961
+ body = self._body() or {}
962
+ plat = str(body.get("platform") or "").strip()
963
+ books = pub_ledger.load_books().get(tid) or {}
964
+ if plat and plat not in books:
965
+ return self._json(400, {"error": "该平台未登记作品"})
966
+ plats = [plat] if plat else list(books)
967
+ if not plats:
968
+ return self._json(400, {"error": "该任务没有已登记作品"})
969
+ manual = bool(body.get("manual"))
970
+ started, skipped = [], {}
971
+ for p in plats:
972
+ ok, err = pub.sync_published_async(tid, p, manual=manual)
973
+ if ok:
974
+ started.append(p)
975
+ else:
976
+ skipped[p] = err
977
+ return self._json(200, {"ok": True, "started": started,
978
+ "skipped": skipped})
842
979
  m = re.match(r"^/api/tasks/([^/]+)/(git-merge|git-discard)$", path)
843
980
  if m:
844
981
  return self._api_git_verdict(m.group(1), m.group(2))
@@ -900,6 +1037,30 @@ class Handler(BaseHTTPRequestHandler):
900
1037
  n = manager.check_updates_async(force=force)
901
1038
  return self._json(200, {"ok": True, "count": n,
902
1039
  "checking": manager.updates_checking()})
1040
+ if path == "/api/catalog/upgrade-all":
1041
+ # 一键升级:把所有可升级条目逐个送进 mgmt 队列(选条见
1042
+ # manager.upgrade_all_targets)。同条目去重闸与单条升级共用——
1043
+ # 忙着的条目跳过并在响应里点名,前端 toast 汇报
1044
+ started, busy, failed = [], [], []
1045
+ for e in manager.upgrade_all_targets():
1046
+ if store.active_mgmt_run(e["id"]):
1047
+ busy.append(e.get("name", e["id"]))
1048
+ continue
1049
+ run = store.create_run("mgmt", "升级 %s" % e.get("name", e["id"]),
1050
+ entry_id=e["id"], op="upgrade")
1051
+ queued, qerr = self._enqueue_run(
1052
+ run["id"], None,
1053
+ {"kind": "mgmt", "run_id": run["id"], "entry_id": e["id"], "op": "upgrade"})
1054
+ if not queued:
1055
+ # 入队失败必须落终态:queued 僵尸会永久堵住去重闸
1056
+ store.update_run(run["id"], status="failed", error=qerr or "enqueue 失败",
1057
+ ended_at=time.strftime("%Y-%m-%d %H:%M:%S"))
1058
+ failed.append(e.get("name", e["id"]))
1059
+ continue
1060
+ started.append({"id": e["id"], "name": e.get("name", e["id"]),
1061
+ "run_id": run["id"]})
1062
+ return self._json(200, {"ok": True, "started": started,
1063
+ "busy": busy, "failed": failed})
903
1064
  if path == "/api/selfupdate/apply":
904
1065
  from core import selfupdate
905
1066
  try:
@@ -1005,6 +1166,15 @@ class Handler(BaseHTTPRequestHandler):
1005
1166
  body.get("name") or "",
1006
1167
  body.get("image_in"))
1007
1168
  return self._json(400, {"error": err}) if err else self._json(200, {"ok": True})
1169
+ if path == "/api/models/set-price":
1170
+ # 手标模型单价(¥/百万 tokens):台账记账与花费预算按此折算
1171
+ from core import modelhub
1172
+ body = self._body()
1173
+ err = modelhub.set_model_price(body.get("provider_id") or "",
1174
+ body.get("name") or "",
1175
+ body.get("price_in"),
1176
+ body.get("price_out"))
1177
+ return self._json(400, {"error": err}) if err else self._json(200, {"ok": True})
1008
1178
  if path == "/api/models/test-provider":
1009
1179
  from core import modelhub
1010
1180
  return self._json(200, modelhub.test_provider(self._body().get("id") or ""))
@@ -1037,6 +1207,33 @@ class Handler(BaseHTTPRequestHandler):
1037
1207
  if err:
1038
1208
  return self._json(400, {"error": err})
1039
1209
  return self._json(200, {"ok": True, "flow": flow})
1210
+ if path == "/api/flows/restore":
1211
+ # 版本历史恢复:把历史版重新 upsert 回来(当前版先入史,可再回滚)
1212
+ body = self._body() or {}
1213
+ flow, err = flows.restore_flow_version(body.get("id") or "",
1214
+ body.get("ts") or "")
1215
+ if err:
1216
+ return self._json(400, {"error": err})
1217
+ return self._json(200, {"ok": True, "flow": flow})
1218
+ if path == "/api/flows/export":
1219
+ # 流程分享码:导出一段可粘贴的码,对端「导入分享码」一键安装
1220
+ body = self._body() or {}
1221
+ code, err = flows.export_flow_code(body.get("id") or "")
1222
+ if err:
1223
+ return self._json(400, {"error": err})
1224
+ return self._json(200, {"ok": True, "code": code,
1225
+ "name": (flows.get_flow(body.get("id") or "") or {}).get("name")})
1226
+ if path == "/api/flows/preview":
1227
+ # 分享码两步导入第一步:解析预览,不落盘
1228
+ res, err = flows.preview_flow_code((self._body() or {}).get("code"))
1229
+ if err:
1230
+ return self._json(400, {"error": err})
1231
+ return self._json(200, dict(ok=True, **res))
1232
+ if path == "/api/flows/import":
1233
+ res, err = flows.import_flow_code((self._body() or {}).get("code"))
1234
+ if err:
1235
+ return self._json(400, {"error": err})
1236
+ return self._json(200, dict(ok=True, **res))
1040
1237
  if path == "/api/skills/lesson-op":
1041
1238
  from core import skills
1042
1239
  body = self._body()
@@ -1295,7 +1492,31 @@ class Handler(BaseHTTPRequestHandler):
1295
1492
  except Exception as e:
1296
1493
  log.exception("backup: 导出失败")
1297
1494
  return self._json(500, {"error": "导出失败:%s" % e})
1495
+ # 远程备份通道:启用即自动推送(失败不挡导出,结果并进响应)
1496
+ try:
1497
+ from core import remotesync
1498
+ info["remote"] = remotesync.push_remote(info.get("path") or "")
1499
+ except Exception:
1500
+ info["remote"] = {"ok": False, "error": "推送异常"}
1298
1501
  return self._json(200, {"ok": True, **info})
1502
+ if path == "/api/data/test-remote":
1503
+ # 远程备份连通性测试:PUT 一个探针文件
1504
+ from core import remotesync
1505
+ return self._json(200, {"ok": True, **remotesync.test_remote()})
1506
+ if path == "/api/data/remote-list":
1507
+ # 远程备份包列举(WebDAV PROPFIND)
1508
+ from core import remotesync
1509
+ names, err = remotesync.list_remote()
1510
+ if err:
1511
+ return self._json(400, {"error": err})
1512
+ return self._json(200, {"ok": True, "files": names})
1513
+ if path == "/api/data/remote-pull":
1514
+ # 拉远程备份包到 imports 目录(之后走既有导入预览/应用向导)
1515
+ from core import remotesync
1516
+ p, err = remotesync.pull_remote((self._body() or {}).get("name"))
1517
+ if err:
1518
+ return self._json(400, {"error": err})
1519
+ return self._json(200, {"ok": True, "path": p})
1299
1520
  if path == "/api/data/import":
1300
1521
  # op=inspect 预览(不落盘);op=apply 应用(merge/replace + 路径重映射)
1301
1522
  from core import backup
@@ -1337,6 +1558,16 @@ class Handler(BaseHTTPRequestHandler):
1337
1558
  from core import zentao
1338
1559
  res = zentao.scan_now() # 网络操作同步做(ThreadingHTTPServer 不堵别的请求)
1339
1560
  return self._json(200, dict(res, ok=bool(res.get("ok"))))
1561
+ if path == "/api/zentao/claims/archive":
1562
+ from core import zentao
1563
+ body = self._body() or {}
1564
+ try:
1565
+ c = zentao.archive_claim(body.get("bug_id"),
1566
+ bool(body.get("archived")),
1567
+ str(body.get("reason") or ""))
1568
+ except zentao.ZenError as e:
1569
+ return self._json(404, {"error": str(e)})
1570
+ return self._json(200, {"ok": True, "claim": c})
1340
1571
  if path == "/api/zentao/modules":
1341
1572
  from core import zentao
1342
1573
  pid = (self._body() or {}).get("product")
@@ -1720,7 +1951,7 @@ class Handler(BaseHTTPRequestHandler):
1720
1951
  # 服务换了个进程(升级重启)——旧蜜蜂自行退场让新服务 spawn 新蜜蜂
1721
1952
  # (旧形象常驻的根因:同端口轮询永远正常,旧进程从不自离)。
1722
1953
  "boot": _BOOT_TS,
1723
- "settings": {"pet_enabled": bool(st.get("pet_enabled", True)),
1954
+ "settings": {"pet_enabled": bool(st.get("pet_enabled", False)),
1724
1955
  "pet_mode": str(st.get("pet_mode") or "always"),
1725
1956
  "pet_skin": str(st.get("pet_skin") or "plush")},
1726
1957
  "workers": {"running": n_run, "queued": n_q},
@@ -1997,7 +2228,7 @@ class Handler(BaseHTTPRequestHandler):
1997
2228
  return self._json(200, {"ok": True, "message": msg})
1998
2229
 
1999
2230
  def _api_run_timeline(self, run_id):
2000
- """直连对话视图的时间线:目标 + 用户消息 + 各步最终回答按序合并成气泡流。
2231
+ """任务对话工作区时间线:目标 + 用户消息 + 各步最终回答按序合并成气泡流。
2001
2232
 
2002
2233
  形状:[任务目标] → [用户消息] → [assistant 输出] → [用户追问] → …。
2003
2234
  步骤正文只认 output/summary(干净回答),供详情页「对话」分区直读。
@@ -2007,14 +2238,17 @@ class Handler(BaseHTTPRequestHandler):
2007
2238
  回放单个 run 会把历史轮的回答全丢掉(2026-09-17 实测:一发消息,
2008
2239
  之前智能体输出从时间线消失)。继承的消息是同 id 副本,跨 run 按
2009
2240
  (id, 时间, 文本) 去重;每个 run 内先消息后步骤(消息总是先于本轮
2010
- 回答送达)。非 direct 任务保持单 run 步骤流(视图未启用)。"""
2241
+ 回答送达)。没有任务归属的管理运行保持单 run 步骤流。"""
2011
2242
  run = store.get_run(run_id)
2012
2243
  if not run:
2013
2244
  return self._json(404, {"error": "not found"})
2014
2245
  task = store.get_task(run.get("task_id") or "") if run.get("task_id") else None
2015
2246
  engine = (task or {}).get("engine") or ""
2016
2247
  items = []
2017
- if engine == "direct":
2248
+ # 对话工作区按任务回放,而不是按某个引擎特判:编排类任务也会
2249
+ # 产生多轮运行(重试、连载续跑、用户反馈),用户需要看到完整的
2250
+ # 智能体输出历史。没有任务归属的管理运行仍只展示自己这一轮。
2251
+ if task:
2018
2252
  runs = list(reversed(store.task_runs(run["task_id"]))) # 旧→新
2019
2253
  else:
2020
2254
  runs = [run]
@@ -2022,7 +2256,7 @@ class Handler(BaseHTTPRequestHandler):
2022
2256
  # 附件归一成 _attachments/ 相对路径:前端展示仍取文件名(attName 剥目录),
2023
2257
  # 但「点击查看」需要相对路径走 /api/runs/<id>/file 通道——只下发文件名的话
2024
2258
  # 点开永远 404。时间取任务创建时刻(对话的开场是任务本身,不是某一轮续跑的起始时间)
2025
- if task and engine == "direct":
2259
+ if task:
2026
2260
  from core import attachments as _att
2027
2261
  atts = [p for p in (_att.norm_rel(a) for a in (task.get("attachments") or [])) if p]
2028
2262
  items.append({
@@ -2070,6 +2304,7 @@ class Handler(BaseHTTPRequestHandler):
2070
2304
  "role": s.get("role") or "",
2071
2305
  "n": s.get("n"),
2072
2306
  "status": s.get("status") or "",
2307
+ "partial": bool(s.get("partial")),
2073
2308
  "text": body,
2074
2309
  "note": s.get("note") or "",
2075
2310
  "run": run_id_of_step,
@@ -2130,13 +2365,23 @@ class Handler(BaseHTTPRequestHandler):
2130
2365
  成没成、跑多久、谁执行的、产出了哪些文件。模型最后一轮回答可能只是
2131
2366
  寒暄/追问(用户反馈:输入"1"跑完 55 秒只见一句"消息可能发错了"),
2132
2367
  执行结果不能依赖模型自觉交代,由产品明示。"""
2133
- if engine != "direct" or not latest:
2368
+ # 对话工作区现在覆盖所有有任务归属的运行(编排、直连、连载等)。
2369
+ # `engine` 仅保留给调用方展示/兼容,不能再把编排任务的结果卡过滤掉。
2370
+ # 没有关联任务的管理运行仍在 _api_run_timeline 中走孤立运行路径,
2371
+ # 不会误显示可续聊的结果卡。
2372
+ if not latest:
2134
2373
  return None
2135
2374
  st = latest.get("status") or ""
2136
2375
  if st not in ("done", "failed", "cancelled", "timeout"):
2137
2376
  return None
2138
2377
  verdict = latest.get("verdict") or {}
2139
2378
  route = latest.get("route") or {}
2379
+ # 非直连编排路径可能只在步骤收尾记录 partial,而没有统一的
2380
+ # run-level verdict。结果卡必须保留这个事实,不能把已执行一部分
2381
+ # 的运行显示成完整成功。
2382
+ partial = bool(verdict.get("partial")) or any(
2383
+ isinstance(step, dict) and bool(step.get("partial"))
2384
+ for step in (latest.get("steps") or []))
2140
2385
 
2141
2386
  def _seconds(value):
2142
2387
  try:
@@ -2183,7 +2428,10 @@ class Handler(BaseHTTPRequestHandler):
2183
2428
  wd, files = "", []
2184
2429
  return {
2185
2430
  "status": st,
2186
- "error": (latest.get("error") or "") if st in ("failed", "timeout") else "",
2431
+ "partial": partial,
2432
+ "error": ((latest.get("error") or "") if st in ("failed", "timeout")
2433
+ else "部分完成:已执行部分操作,但最终总结未完整生成"
2434
+ if partial else ""),
2187
2435
  "executor": route.get("implementer") or verdict.get("impl") or "",
2188
2436
  "turns": verdict.get("turns") or 0,
2189
2437
  "duration_s": duration_s,
@@ -2205,8 +2453,8 @@ class Handler(BaseHTTPRequestHandler):
2205
2453
  return self._json(404, {"error": "not found"})
2206
2454
  task = store.get_task(run.get("task_id") or "") if run.get("task_id") else None
2207
2455
  is_serial = bool(task and task.get("serial"))
2208
- if not task or (task.get("engine") != "direct" and not is_serial):
2209
- return self._json(400, {"error": "该任务不是直连任务,请用「下达指令」"})
2456
+ if not task:
2457
+ return self._json(400, {"error": "运行没有关联任务,无法进入对话工作区"})
2210
2458
  if (run.get("status") or "") in ("queued", "running"):
2211
2459
  return self._json(400, {"error": "运行中:消息会随下一步自动送达,无需追话"})
2212
2460
  body = self._body() or {}
@@ -2293,6 +2541,7 @@ class Handler(BaseHTTPRequestHandler):
2293
2541
  pass
2294
2542
  cid = self._client_id()
2295
2543
  ver = store.state_version()
2544
+ uver = usage_ledger.usage_version()
2296
2545
  sent_ctrl = None
2297
2546
  last_send = 0.0
2298
2547
  try:
@@ -2308,8 +2557,9 @@ class Handler(BaseHTTPRequestHandler):
2308
2557
  last_send = time.time()
2309
2558
  while True:
2310
2559
  ver = store.wait_state_change(ver, 2.0)
2560
+ uver_now = usage_ledger.usage_version()
2311
2561
  ctrl = remote.control_view(cid)
2312
- if ver == store.state_version() and ctrl == sent_ctrl:
2562
+ if ver == store.state_version() and ctrl == sent_ctrl and uver_now == uver:
2313
2563
  # 空闲心跳:真发出去的字节让中间层不掐空闲流,也让半死
2314
2564
  # 连接在下一次写时尽早报错(注释行,EventSource 不派发事件)
2315
2565
  if time.time() - last_send >= SSE_PING_S:
@@ -2317,6 +2567,18 @@ class Handler(BaseHTTPRequestHandler):
2317
2567
  self.wfile.flush()
2318
2568
  last_send = time.time()
2319
2569
  continue # 超时醒来且无变化
2570
+ if uver_now != uver:
2571
+ # 用量条实时化:台账有新记账(哪怕状态没变,如内置智能体
2572
+ # 同一工具循环内多次模型调用)只发一个几十字节的 usage
2573
+ # 事件,前端据此刷新左下角用量条,不构建/推送全量状态。
2574
+ # 2s 醒来才查一次版本号:两次醒来之间落多少条记录都合并
2575
+ # 成一个事件,推送频率天然有上界。
2576
+ self.wfile.write(("event: usage\ndata: " +
2577
+ json.dumps({"v": uver_now}) + "\n\n").encode("utf-8"))
2578
+ uver = uver_now
2579
+ last_send = time.time()
2580
+ if ver == store.state_version() and ctrl == sent_ctrl:
2581
+ continue # 只需要补 usage 事件,状态与控制权都没变
2320
2582
  payload = json.dumps(_state_payload(cid, ver), ensure_ascii=False)
2321
2583
  self.wfile.write(("data: " + payload + "\n\n").encode("utf-8"))
2322
2584
  self.wfile.flush()
@@ -2558,9 +2820,9 @@ def _state_payload(client_id="", ver=None):
2558
2820
  return {
2559
2821
  "v": ver,
2560
2822
  "agents": agents,
2561
- "tasks": store.list_tasks(30, archived=False),
2823
+ "tasks": store.list_tasks(archived=False),
2562
2824
  # 归档任务默认隐藏;目标/上下文可能很大,只有查看或恢复时再按需读取。
2563
- "archived_tasks": [_state_task_summary(t) for t in store.list_tasks(30, archived=True)],
2825
+ "archived_tasks": [_state_task_summary(t) for t in store.list_tasks(archived=True)],
2564
2826
  # 每个任务的最近一次运行(不受 runs 窗口限制):侧栏靠它展示各任务真实近况
2565
2827
  "task_latest": {tid: _state_run_summary(run) for tid, run in latest.items()},
2566
2828
  # 每个任务的运行次数/步骤总数(全量):侧栏「查看全部」的计数来源
@@ -2651,7 +2913,7 @@ def _start_pet_keeper(port):
2651
2913
  def _loop():
2652
2914
  while True:
2653
2915
  try:
2654
- if settings.load().get("pet_enabled", True):
2916
+ if settings.load().get("pet_enabled", False):
2655
2917
  if _PET_PROC is None or _PET_PROC.poll() is not None:
2656
2918
  # 全机唯一蜜蜂:别的服务实例(不同数据目录)已养蜂时
2657
2919
  # 不再拉起,否则桌面会出现多只(用户实测踩坑)
@@ -2751,6 +3013,43 @@ def main():
2751
3013
  from core import usage
2752
3014
  store.set_run_estimator(usage.estimate)
2753
3015
  store.set_run_auditor(usage.audit_run) # 终态把预估与真实用量逐条对账
3016
+ flows.set_runs_provider(store.list_runs) # 版本战绩对账用 run 遍历器(防 flows→store 静态环)
3017
+ from core import evalbench as _evalbench
3018
+ automation.register_tick_hook(_evalbench.fire_due) # 评测台定时回归挂自动化 tick
3019
+ from core import notify as _notify
3020
+ usage_ledger.set_alert_push(_notify.push_text) # 花费预警推送注入(防 usage→notify 静态环)
3021
+ from core import mcp_client as _mcp
3022
+ def _mcp_servers_text():
3023
+ """settings 清单 + 已启用插件的 MCP 服务器合成(plugin 名隔离防撞名)。"""
3024
+ import json as _json
3025
+ try:
3026
+ raw = str(settings.load().get("mcp_servers") or "")
3027
+ configured = _json.loads(raw) if raw.strip() else []
3028
+ except Exception:
3029
+ return raw or ""
3030
+ if not isinstance(configured, list):
3031
+ return raw or ""
3032
+ try:
3033
+ from core import plugins as _plugins
3034
+ plugin_servers = _plugins.active_mcp_servers()
3035
+ except Exception:
3036
+ plugin_servers = [] # 坏插件不能连累用户显式配置的服务器
3037
+ import hashlib as _hashlib
3038
+ import re as _re
3039
+ for item in plugin_servers or []:
3040
+ if not isinstance(item, dict):
3041
+ continue
3042
+ plugin_id = str(item.pop("plugin_id", "plugin"))
3043
+ server_name = str(item.get("name") or "server")
3044
+ digest = _hashlib.sha256(
3045
+ (plugin_id + ":" + server_name).encode("utf-8")).hexdigest()[:6]
3046
+ safe = _re.sub(r"[^a-z0-9_-]", "-", plugin_id.lower())
3047
+ item["name"] = ("plg_" + safe[:11] + "_" + digest)[:24]
3048
+ if not any(x.get("name") == item["name"] for x in configured
3049
+ if isinstance(x, dict)):
3050
+ configured.append(item)
3051
+ return _json.dumps(configured, ensure_ascii=False)
3052
+ _mcp.set_settings_text(_mcp_servers_text) # MCP 清单注入(防 mcp_client→settings 边)
2754
3053
  _step("正在回填用量台账…")
2755
3054
  n_bf = usage.backfill_from_runs() # 历史运行 token 回填台账(幂等,仅补缺失步骤)
2756
3055
  if n_bf: