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.
- package/CHANGELOG.md +589 -543
- package/LICENSE +21 -21
- package/README.md +509 -508
- package/app/.playwright-cli/console-2026-09-23T08-26-03-635Z.log +88 -0
- package/app/.playwright-cli/page-2026-09-23T08-26-03-845Z.yml +126 -0
- package/app/.playwright-cli/page-2026-09-23T08-29-44-203Z.png +0 -0
- package/app/.playwright-cli/page-2026-09-23T08-35-42-046Z.yml +210 -0
- package/app/.playwright-cli/page-2026-09-23T08-36-52-858Z.png +0 -0
- package/app/.playwright-cli/page-2026-09-23T08-37-33-938Z.png +0 -0
- package/app/.playwright-cli/run-detail-after.png +0 -0
- package/app/browser-final-1440.png +0 -0
- package/app/browser-final-390.png +0 -0
- package/app/browser-google-fallback.png +0 -0
- package/app/browser-google-test.png +0 -0
- package/app/browser-workbench-1440.png +0 -0
- package/app/browser-workbench-390.png +0 -0
- package/app/browser-workspace-google.png +0 -0
- package/app/browser-workspace-smoke.png +0 -0
- package/app/browser-workspace.png +0 -0
- package/app/core/acceptance.py +100 -100
- package/app/core/aiflavor.py +197 -197
- package/app/core/artifacts.py +148 -148
- package/app/core/assets.py +110 -110
- package/app/core/attachments.py +623 -623
- package/app/core/audit.py +30 -0
- package/app/core/automation.py +107 -12
- package/app/core/backup.py +470 -470
- package/app/core/beekeeper.py +183 -183
- package/app/core/benchstore.py +95 -0
- package/app/core/board.py +209 -209
- package/app/core/bookmeta.py +935 -930
- package/app/core/bookmeta_catalog.py +180 -180
- package/app/core/branching.py +137 -137
- package/app/core/builtin_agent.py +1673 -1234
- package/app/core/capability.py +144 -144
- package/app/core/catalog.py +7 -1
- package/app/core/chaptersafe.py +32 -32
- package/app/core/cleanup.py +303 -303
- package/app/core/compaction.py +201 -201
- package/app/core/covergen.py +356 -356
- package/app/core/defectretro.py +43 -43
- package/app/core/diagnostics.py +114 -114
- package/app/core/dispatch.py +190 -173
- package/app/core/dispatch_log.py +128 -128
- package/app/core/env_scrub.py +85 -85
- package/app/core/error_codes.py +11 -2
- package/app/core/errorlog.py +137 -137
- package/app/core/evalbench.py +720 -0
- package/app/core/evaluation.py +174 -174
- package/app/core/failure_notify.py +290 -290
- package/app/core/flows.py +234 -9
- package/app/core/goal_service.py +167 -167
- package/app/core/health.py +374 -374
- package/app/core/history.py +38 -38
- package/app/core/hooks.py +162 -162
- package/app/core/jobs.py +22 -1
- package/app/core/manager.py +2143 -2122
- package/app/core/market_remote.py +10 -0
- package/app/core/mcp_client.py +287 -0
- package/app/core/mocks.py +64 -64
- package/app/core/modelhub.py +185 -2
- package/app/core/notify.py +201 -106
- package/app/core/novel_quality.py +189 -0
- package/app/core/operations.py +228 -228
- package/app/core/paihang.py +138 -138
- package/app/core/paths.py +4 -0
- package/app/core/pipeline.py +426 -28
- package/app/core/planner.py +243 -22
- package/app/core/plugins.py +525 -0
- package/app/core/portguard.py +111 -111
- package/app/core/preview.py +293 -293
- package/app/core/publish/__init__.py +7 -7
- package/app/core/publish/auto.py +367 -367
- package/app/core/publish/browser.py +655 -539
- package/app/core/publish/fanqie.py +165 -159
- package/app/core/publish/flow.py +589 -440
- package/app/core/publish/flows-fanqie-calibrated.json +336 -332
- package/app/core/publish/ledger.py +229 -203
- package/app/core/publish/manager.py +824 -692
- package/app/core/publish/qimao.py +146 -141
- package/app/core/publish/ws.py +139 -139
- package/app/core/redact.py +38 -38
- package/app/core/remotesync.py +121 -0
- package/app/core/repeat_guard.py +129 -129
- package/app/core/router.py +398 -398
- package/app/core/runner.py +2142 -2067
- package/app/core/selfupdate.py +384 -384
- package/app/core/session_log.py +161 -161
- package/app/core/sessions.py +312 -312
- package/app/core/settings.py +42 -4
- package/app/core/settings_schema.py +270 -262
- package/app/core/share_page.py +114 -114
- package/app/core/skill_scan.py +122 -122
- package/app/core/skillpacks/fanqie-novel.md +80 -80
- package/app/core/skillpacks/market/character-bible.md +66 -66
- package/app/core/skillpacks/market/code-risk-checklist.md +58 -58
- package/app/core/skillpacks/market/git-workflow.md +57 -57
- package/app/core/skillpacks/market/release-notes.md +72 -72
- package/app/core/skillpacks/market/weekly-report.md +71 -71
- package/app/core/skillpacks/market/worldview-consistency.md +70 -70
- package/app/core/step_runner.py +132 -132
- package/app/core/store.py +30 -8
- package/app/core/task_compile.py +199 -199
- package/app/core/telemetry.py +292 -292
- package/app/core/tlsctx.py +64 -64
- package/app/core/token_meter.py +147 -147
- package/app/core/tracing.py +144 -144
- package/app/core/upstream.py +41 -41
- package/app/core/usage.py +1035 -862
- package/app/core/volumes.py +389 -389
- package/app/core/wxdigest.py +858 -858
- package/app/core/zentao.py +2614 -2465
- package/app/current-browser.png +0 -0
- package/app/main.py +316 -17
- package/app/mcp_server.py +175 -0
- package/app/pet.py +83 -22
- package/app/pick_dialog.py +110 -110
- package/app/plugins/code-review-helper/.codex-plugin/plugin.json +18 -0
- package/app/plugins/code-review-helper/skills/review.md +16 -0
- package/app/ui/app.js +1391 -77
- package/app/ui/artdiff.js +74 -74
- package/app/ui/board.html +162 -162
- package/app/ui/board.js +305 -305
- package/app/ui/i18n.js +249 -7
- package/app/ui/icons/bee.svg +79 -79
- package/app/ui/index.html +277 -13
- package/app/ui/manifest.json +16 -16
- package/app/ui/qrcode.js +2297 -2297
- package/app/ui/style.css +845 -12
- package/app//344/275/234/345/223/201/344/277/241/346/201/257-/344/270/203/347/214/253.md +18 -0
- package/bin/tutti.js +147 -147
- package/package.json +47 -46
- 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
|
-
|
|
518
|
-
|
|
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
|
-
|
|
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",
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
"
|
|
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
|
|
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(
|
|
2823
|
+
"tasks": store.list_tasks(archived=False),
|
|
2562
2824
|
# 归档任务默认隐藏;目标/上下文可能很大,只有查看或恢复时再按需读取。
|
|
2563
|
-
"archived_tasks": [_state_task_summary(t) for t in store.list_tasks(
|
|
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",
|
|
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:
|