codebee 0.1.72 → 0.1.74
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 +12 -0
- package/README.md +4 -4
- package/app/core/analytics.py +38 -0
- package/app/core/bookmeta.py +0 -14
- package/app/core/contracts.py +255 -0
- package/app/core/error_codes.py +9 -0
- package/app/core/flows.py +1 -1
- package/app/core/health.py +14 -1
- package/app/core/jobs.py +54 -1
- package/app/core/manager.py +106 -17
- package/app/core/market.py +2 -2
- package/app/core/modelhub.py +104 -9
- package/app/core/openapi.py +154 -0
- package/app/core/paihang.py +8 -1
- package/app/core/pipeline.py +41 -0
- package/app/core/planner.py +2 -0
- package/app/core/plugins.py +34 -0
- package/app/core/presence.py +77 -0
- package/app/core/publish/auto.py +11 -0
- package/app/core/publish/flow.py +54 -3
- package/app/core/publish/manager.py +215 -28
- package/app/core/publish/qimao.py +30 -0
- package/app/core/retrieval.py +137 -0
- package/app/core/runner.py +85 -2
- package/app/core/skillpacks/fanqie-novel.md +66 -48
- package/app/core/skillpacks/qimao-signing.md +70 -59
- package/app/core/store.py +72 -0
- package/app/core/story_tracking.py +241 -0
- package/app/main.py +205 -48
- package/app/mcp_server.py +75 -6
- package/app/ui/app.js +390 -101
- package/app/ui/i18n.js +36 -7
- package/app/ui/index.html +8 -3
- package/app/ui/style.css +7059 -7040
- package/package.json +1 -1
package/app/main.py
CHANGED
|
@@ -247,6 +247,18 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
247
247
|
return self._json(401, {"error": "需要访问令牌(启动 CodeBee 时控制台会显示)"})
|
|
248
248
|
if path == "/api/state":
|
|
249
249
|
return self._json(200, _state_payload(self._client_id()))
|
|
250
|
+
if path == "/api/openapi.json":
|
|
251
|
+
from core import openapi
|
|
252
|
+
return self._json(200, openapi.document())
|
|
253
|
+
m = re.match(r"^/api/a2a/tasks/([^/]+)$", path)
|
|
254
|
+
if m:
|
|
255
|
+
task = store.get_task(m.group(1))
|
|
256
|
+
if not task:
|
|
257
|
+
return self._json(404, {"error": "not found"})
|
|
258
|
+
run = store.latest_run_by_task().get(m.group(1))
|
|
259
|
+
from core import contracts
|
|
260
|
+
return self._json(200, {"task": task, "run": run,
|
|
261
|
+
"contract": contracts.get(m.group(1))})
|
|
250
262
|
if path == "/api/browse":
|
|
251
263
|
return self._api_browse()
|
|
252
264
|
if path == "/api/dir/scan":
|
|
@@ -266,6 +278,9 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
266
278
|
if path == "/api/board":
|
|
267
279
|
# 任务驾驶舱大屏的轻量聚合(KB 级;别让它去拉 /api/state 全量)
|
|
268
280
|
return self._json(200, board.payload())
|
|
281
|
+
if path == "/api/analytics":
|
|
282
|
+
from core import analytics
|
|
283
|
+
return self._json(200, analytics.summary(store))
|
|
269
284
|
if path == "/api/connect":
|
|
270
285
|
# 供设置页「手机连接」弹框生成二维码;远程打开需令牌,天然受保护
|
|
271
286
|
return self._json(200, {"urls": remote.build_connect_urls(PORT),
|
|
@@ -303,9 +318,27 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
303
318
|
except Exception:
|
|
304
319
|
pass
|
|
305
320
|
return self._json(200, v)
|
|
321
|
+
if path == "/api/plugins":
|
|
322
|
+
# 本地插件(.codex-plugin/plugin.json 契约):发现+安装台账的
|
|
323
|
+
# 聚合视图。坏 manifest 以 blocked/broken 行呈现,不 500。
|
|
324
|
+
from core import plugins
|
|
325
|
+
return self._json(200, plugins.view())
|
|
306
326
|
if path == "/api/knowledge":
|
|
307
327
|
from core import knowledge
|
|
308
328
|
return self._json(200, knowledge.view())
|
|
329
|
+
if path == "/api/agents/presence":
|
|
330
|
+
from core import presence
|
|
331
|
+
return self._json(200, {"agents": presence.list_agents()})
|
|
332
|
+
if path == "/api/retrieval/search":
|
|
333
|
+
from core import retrieval
|
|
334
|
+
q = parse_qs(urlparse(self.path).query)
|
|
335
|
+
try:
|
|
336
|
+
results = retrieval.search((q.get("q") or [""])[0],
|
|
337
|
+
(q.get("limit") or [10])[0])
|
|
338
|
+
except ValueError as exc:
|
|
339
|
+
return self._json(400, {"error": str(exc)})
|
|
340
|
+
return self._json(200, {"query": (q.get("q") or [""])[0][:500],
|
|
341
|
+
"results": results})
|
|
309
342
|
if path == "/api/settings":
|
|
310
343
|
return self._json(200, dict(settings.load(), **jobs.workers_info()))
|
|
311
344
|
if path == "/api/settings-v2":
|
|
@@ -532,7 +565,26 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
532
565
|
m = re.match(r"^/api/tasks/([^/]+)/detail$", path)
|
|
533
566
|
if m:
|
|
534
567
|
task = store.get_task(m.group(1))
|
|
535
|
-
|
|
568
|
+
if not task:
|
|
569
|
+
return self._json(404, {"error": "not found"})
|
|
570
|
+
from core import contracts
|
|
571
|
+
return self._json(200, {"task": task, "contract": contracts.get(m.group(1))})
|
|
572
|
+
m = re.match(r"^/api/tasks/([^/]+)/contract$", path)
|
|
573
|
+
if m:
|
|
574
|
+
from core import contracts
|
|
575
|
+
item = contracts.get(m.group(1))
|
|
576
|
+
return self._json(200, {"contract": item}) if item else self._json(404, {"error": "not found"})
|
|
577
|
+
m = re.match(r"^/api/tasks/([^/]+)/story-tracking$", path)
|
|
578
|
+
if m:
|
|
579
|
+
from core import story_tracking
|
|
580
|
+
task = store.get_task(m.group(1))
|
|
581
|
+
if not task:
|
|
582
|
+
return self._json(404, {"error": "not found"})
|
|
583
|
+
try:
|
|
584
|
+
return self._json(200, {"state": story_tracking.load(task.get("workdir")),
|
|
585
|
+
"check": story_tracking.check(task.get("workdir"))})
|
|
586
|
+
except ValueError as exc:
|
|
587
|
+
return self._json(409, {"error": str(exc)})
|
|
536
588
|
m = re.match(r"^/api/tasks/([^/]+)/runs$", path)
|
|
537
589
|
if m:
|
|
538
590
|
# 任务级详情用:该任务全部 run(含 steps),不受前端 run 窗口限制
|
|
@@ -540,13 +592,18 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
540
592
|
return self._json(404, {"error": "not found"})
|
|
541
593
|
query = parse_qs(urlparse(self.path).query)
|
|
542
594
|
if "limit" not in query:
|
|
543
|
-
|
|
595
|
+
from core import contracts
|
|
596
|
+
return self._json(200, {"runs": store.task_runs(m.group(1)),
|
|
597
|
+
"contract": contracts.get(m.group(1))})
|
|
544
598
|
try:
|
|
545
599
|
limit = max(1, min(20, int((query.get("limit") or [3])[0])))
|
|
546
600
|
offset = max(0, int((query.get("offset") or [0])[0]))
|
|
547
601
|
except (TypeError, ValueError):
|
|
548
602
|
return self._json(400, {"error": "invalid pagination"})
|
|
549
|
-
|
|
603
|
+
payload = store.task_runs_page(m.group(1), offset, limit)
|
|
604
|
+
from core import contracts
|
|
605
|
+
payload["contract"] = contracts.get(m.group(1))
|
|
606
|
+
return self._json(200, payload)
|
|
550
607
|
m = re.match(r"^/api/tasks/([^/]+)/bible$", path)
|
|
551
608
|
if m:
|
|
552
609
|
# 故事圣经:查看(无令牌豁免走 _authed 已过;本机免令牌)
|
|
@@ -557,14 +614,15 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
557
614
|
m = re.match(r"^/api/tasks/([^/]+)/book-meta$", path)
|
|
558
615
|
if m:
|
|
559
616
|
# 作品信息(番茄/七猫建书表单资料):读取生成状态与结果。
|
|
560
|
-
#
|
|
617
|
+
# 生成可从链上任意任务触发、落点在根任务(都是用第一个),
|
|
618
|
+
# running/failed 状态也要沿链可见(chain_book_meta)。
|
|
561
619
|
task = store.get_task(m.group(1))
|
|
562
620
|
if not task:
|
|
563
621
|
return self._json(404, {"error": "not found"})
|
|
564
622
|
return self._json(200, {"book_meta": {
|
|
565
|
-
p: store.
|
|
623
|
+
p: store.chain_book_meta(task, p)
|
|
566
624
|
for p in ("fanqie", "qimao")
|
|
567
|
-
if store.
|
|
625
|
+
if store.chain_book_meta(task, p)}})
|
|
568
626
|
if path == "/api/publish":
|
|
569
627
|
# 一键发布:平台连接状态 + 最近台账(详情页发布面板)
|
|
570
628
|
from core.publish import manager as pub
|
|
@@ -758,6 +816,9 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
758
816
|
return deny
|
|
759
817
|
if path == "/api/tasks":
|
|
760
818
|
return self._api_create_task()
|
|
819
|
+
if path == "/api/a2a/tasks":
|
|
820
|
+
status, resp = self._create_and_start(self._body() or {})
|
|
821
|
+
return self._json(status, {"protocol": "a2a-lite", **resp})
|
|
761
822
|
if path == "/api/hooks/save":
|
|
762
823
|
# 生命周期钩子配置整表保存(本机设置)
|
|
763
824
|
from core import hooks
|
|
@@ -775,6 +836,87 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
775
836
|
return self._json(200, {"ok": ok, "output": out})
|
|
776
837
|
if path == "/api/tasks/clarify":
|
|
777
838
|
return self._api_task_clarify()
|
|
839
|
+
if path == "/api/agents/presence":
|
|
840
|
+
from core import presence
|
|
841
|
+
body = self._body() or {}
|
|
842
|
+
try:
|
|
843
|
+
row = presence.heartbeat(body.get("agent_id"), body.get("label"),
|
|
844
|
+
body.get("capabilities"), body.get("task_id"),
|
|
845
|
+
body.get("status"), body.get("metadata"))
|
|
846
|
+
except ValueError as exc:
|
|
847
|
+
return self._json(400, {"error": str(exc)})
|
|
848
|
+
return self._json(200, {"ok": True, "agent": row})
|
|
849
|
+
if path == "/api/retrieval/index":
|
|
850
|
+
from core import retrieval
|
|
851
|
+
body = self._body() or {}
|
|
852
|
+
try:
|
|
853
|
+
if body.get("source"):
|
|
854
|
+
row = retrieval.upsert(body.get("source"), body.get("text"), body.get("metadata"), body.get("doc_id"))
|
|
855
|
+
return self._json(200, {"ok": True, "document": row})
|
|
856
|
+
root = body.get("root")
|
|
857
|
+
if not root:
|
|
858
|
+
raise ValueError("root 必填")
|
|
859
|
+
# Indexing is intentionally scoped to the configured workspace
|
|
860
|
+
# or an existing task workdir; never accept an arbitrary disk path.
|
|
861
|
+
allowed = [settings.default_workdir()]
|
|
862
|
+
task_id = str(body.get("task_id") or "").strip()
|
|
863
|
+
if task_id:
|
|
864
|
+
task = store.get_task(task_id)
|
|
865
|
+
if not task:
|
|
866
|
+
return self._json(404, {"error": "任务不存在"})
|
|
867
|
+
allowed.append(task.get("workdir"))
|
|
868
|
+
result = retrieval.index_directory(root, limit=body.get("limit") or 500,
|
|
869
|
+
allowed_roots=allowed)
|
|
870
|
+
except (ValueError, TypeError) as exc:
|
|
871
|
+
return self._json(400, {"error": str(exc)})
|
|
872
|
+
return self._json(200, {"ok": True, **result})
|
|
873
|
+
m = re.match(r"^/api/tasks/([^/]+)/contract$", path)
|
|
874
|
+
if m:
|
|
875
|
+
from core import contracts
|
|
876
|
+
body = self._body() or {}
|
|
877
|
+
op = str(body.get("op") or "update").lower()
|
|
878
|
+
tid = m.group(1)
|
|
879
|
+
try:
|
|
880
|
+
if op in ("update", "create"):
|
|
881
|
+
item = contracts.create(tid, body)
|
|
882
|
+
return self._json(200, {"ok": True, "contract": item})
|
|
883
|
+
if op == "evidence":
|
|
884
|
+
item = contracts.add_evidence(tid, body.get("evidence") or body,
|
|
885
|
+
actor=body.get("actor") or self._client_name())
|
|
886
|
+
return self._json(200, {"ok": True, "contract": item})
|
|
887
|
+
if op == "approve":
|
|
888
|
+
item, err = contracts.approve(tid, body.get("actor") or self._client_name(), body.get("note"))
|
|
889
|
+
elif op == "reject":
|
|
890
|
+
item, err = contracts.reject(tid, body.get("actor") or self._client_name(), body.get("note"))
|
|
891
|
+
elif op == "blocked":
|
|
892
|
+
item, err = contracts.set_blocked(tid, body.get("reason"), body.get("actor") or self._client_name())
|
|
893
|
+
else:
|
|
894
|
+
return self._json(400, {"error": "未知 contract 操作"})
|
|
895
|
+
except ValueError as exc:
|
|
896
|
+
return self._json(400, {"error": str(exc)})
|
|
897
|
+
return self._json(400 if err else 200, {"error": err} if err else {"ok": True, "contract": item})
|
|
898
|
+
m = re.match(r"^/api/tasks/([^/]+)/story-tracking$", path)
|
|
899
|
+
if m:
|
|
900
|
+
from core import story_tracking
|
|
901
|
+
task = store.get_task(m.group(1))
|
|
902
|
+
if not task:
|
|
903
|
+
return self._json(404, {"error": "not found"})
|
|
904
|
+
body = self._body() or {}
|
|
905
|
+
try:
|
|
906
|
+
if body.get("op") == "init":
|
|
907
|
+
state = story_tracking.init(task.get("workdir"), task.get("id"),
|
|
908
|
+
task.get("title"), body.get("premise") or task.get("goal"))
|
|
909
|
+
else:
|
|
910
|
+
state = story_tracking.commit_chapter(
|
|
911
|
+
task.get("workdir"), body.get("chapter"), body.get("outline"), body.get("prose"),
|
|
912
|
+
facts=body.get("facts"), characters=body.get("characters"),
|
|
913
|
+
timeline=body.get("timeline"), foreshadowing=body.get("foreshadowing"),
|
|
914
|
+
next_promises=body.get("next_promises"), author_truth=body.get("author_truth"),
|
|
915
|
+
reader_known=body.get("reader_known"))
|
|
916
|
+
except (ValueError, OSError) as exc:
|
|
917
|
+
return self._json(400, {"error": str(exc)})
|
|
918
|
+
return self._json(200, {"ok": True, "state": state,
|
|
919
|
+
"check": story_tracking.check(task.get("workdir"))})
|
|
778
920
|
if path == "/api/notify/test":
|
|
779
921
|
# 设置页「发送测试」:向全部已配置推送通道发一条测试消息
|
|
780
922
|
from core import notify
|
|
@@ -1048,6 +1190,12 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
1048
1190
|
store.update_run(m.group(1), cancelled_by_user=True)
|
|
1049
1191
|
ok = jobs.cancel(m.group(1))
|
|
1050
1192
|
return self._json(200, {"ok": ok})
|
|
1193
|
+
m = re.match(r"^/api/runs/([^/]+)/resume_now$", path)
|
|
1194
|
+
if m:
|
|
1195
|
+
# 立即重试:跳过自动续跑的退避等待马上入队(用户点按钮 = 主动
|
|
1196
|
+
# 选择不等退避窗口)。设备控制权由 do_POST 顶部的统一写闸把关。
|
|
1197
|
+
ok, err = jobs.enqueue_now(m.group(1))
|
|
1198
|
+
return self._json(400, {"error": err}) if not ok else self._json(200, {"ok": True})
|
|
1051
1199
|
m = re.match(r"^/api/runs/([^/]+)/delete$", path)
|
|
1052
1200
|
if m:
|
|
1053
1201
|
ok, err = store.delete_run(m.group(1))
|
|
@@ -1306,6 +1454,23 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
1306
1454
|
from core import knowledge
|
|
1307
1455
|
n = knowledge.learn_from_run((self._body().get("run_id") or ""))
|
|
1308
1456
|
return self._json(200, {"ok": True, "learned": n})
|
|
1457
|
+
m = re.match(r"^/api/plugins/([^/]+)/(install|enable|disable|remove)$", path)
|
|
1458
|
+
if m:
|
|
1459
|
+
# 本地插件生命周期:只搬文件+记账(skills 走 market.install_files、
|
|
1460
|
+
# MCP 只暴露声明),永不执行插件代码。前端 loadPlugins 拉的就是
|
|
1461
|
+
# 上面 GET /api/plugins;缺这组路由时设置页会显示「插件加载失败」。
|
|
1462
|
+
from core import plugins
|
|
1463
|
+
pid, op = m.group(1), m.group(2)
|
|
1464
|
+
if op == "install":
|
|
1465
|
+
result, err = plugins.install(pid)
|
|
1466
|
+
if err:
|
|
1467
|
+
return self._json(400, {"error": err})
|
|
1468
|
+
return self._json(200, result or {"ok": True})
|
|
1469
|
+
if op == "remove":
|
|
1470
|
+
err = plugins.remove(pid)
|
|
1471
|
+
return self._json(400, {"error": err}) if err else self._json(200, {"ok": True})
|
|
1472
|
+
err = plugins.toggle(pid, op == "enable")
|
|
1473
|
+
return self._json(400, {"error": err}) if err else self._json(200, {"ok": True})
|
|
1309
1474
|
m = re.match(r"^/api/flows/([^/]+)/reset$", path)
|
|
1310
1475
|
if m:
|
|
1311
1476
|
err = flows.reset_flow(m.group(1))
|
|
@@ -1689,6 +1854,13 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
1689
1854
|
task = store.get_task(task_id)
|
|
1690
1855
|
if not task:
|
|
1691
1856
|
return self._json(404, {"error": "任务不存在"})
|
|
1857
|
+
if op == "git-merge":
|
|
1858
|
+
from core import contracts
|
|
1859
|
+
contract = contracts.get(task_id)
|
|
1860
|
+
if (contract and contract.get("approval_required")) or task.get("approval_required"):
|
|
1861
|
+
if not contracts.release_allowed(task_id):
|
|
1862
|
+
return self._json(409, {"error": "任务需要先完成审批,才能合并产物",
|
|
1863
|
+
"contract": contract})
|
|
1692
1864
|
if op == "git-merge":
|
|
1693
1865
|
from core import operations
|
|
1694
1866
|
operation_id = operations.begin(
|
|
@@ -1751,7 +1923,10 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
1751
1923
|
|
|
1752
1924
|
后台线程跑(编排者→作者CLI→模板的降级链可能数分钟),请求立即返回;
|
|
1753
1925
|
前端靠任务 book_meta 状态(SSE 全量状态里带)轮进度。已有 running 时
|
|
1754
|
-
|
|
1926
|
+
幂等拒绝,不重复起线程。生成权不限定首批(2026-09-30 实案:
|
|
1927
|
+
start_chapter>1 的独立任务被误拦,用户点首版也撞墙)——任何任务都能
|
|
1928
|
+
触发;但开书资料属于「这本书」,素材与落点统一取连载链根任务
|
|
1929
|
+
(都是用第一个),续写批次触发的生成整链可见、建书口径不分叉。"""
|
|
1755
1930
|
from core import bookmeta
|
|
1756
1931
|
task = store.get_task(task_id)
|
|
1757
1932
|
if not task:
|
|
@@ -1760,27 +1935,22 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
1760
1935
|
platform = (body.get("platform") or "").strip()
|
|
1761
1936
|
if platform not in bookmeta.PLATFORMS:
|
|
1762
1937
|
return self._json(400, {"error": "platform 必须是 fanqie 或 qimao"})
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
return self._json(400, {"error": (
|
|
1768
|
-
"开书资料在连载首批任务《%s》上生成与维护,请到首批任务的「作品信息」页操作"
|
|
1769
|
-
% ((root or {}).get("title") or "首批")
|
|
1770
|
-
if root else "只有连载首批任务需要作品信息;续写批次沿用第一批的开书资料")})
|
|
1771
|
-
if task.get("status") in ("queued", "running"):
|
|
1938
|
+
chain = store.serial_chain_ids(task_id)
|
|
1939
|
+
target = (store.get_task(chain[0]) if chain else None) or task
|
|
1940
|
+
target_id = str(target.get("id"))
|
|
1941
|
+
if target.get("status") in ("queued", "running"):
|
|
1772
1942
|
return self._json(400, {"error": "任务正在运行,请等本轮结束后再生成作品信息"})
|
|
1773
|
-
cur = ((
|
|
1943
|
+
cur = ((target.get("book_meta") or {}).get(platform) or {})
|
|
1774
1944
|
if cur.get("status") == "running":
|
|
1775
1945
|
return self._json(200, {"ok": True, "already": True})
|
|
1776
|
-
if not store.set_book_meta(
|
|
1946
|
+
if not store.set_book_meta(target_id, platform,
|
|
1777
1947
|
{"status": "running",
|
|
1778
1948
|
"at": time.strftime("%Y-%m-%d %H:%M:%S")}):
|
|
1779
1949
|
return self._json(404, {"error": "任务不存在"})
|
|
1780
|
-
author = bookmeta._resolve_author(
|
|
1950
|
+
author = bookmeta._resolve_author(target)
|
|
1781
1951
|
threading.Thread(target=bookmeta.generate_async, daemon=True,
|
|
1782
1952
|
name="book-meta-%s" % platform,
|
|
1783
|
-
args=(
|
|
1953
|
+
args=(target_id, platform, author)).start()
|
|
1784
1954
|
return self._json(200, {"ok": True, "started": True})
|
|
1785
1955
|
|
|
1786
1956
|
def _api_publish_platform_op(self, platform, op):
|
|
@@ -1815,6 +1985,12 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
1815
1985
|
force = bool(body.get("force"))
|
|
1816
1986
|
force_confirmed = bool(body.get("force_confirmed"))
|
|
1817
1987
|
force_reason = str(body.get("force_reason") or "").strip()
|
|
1988
|
+
from core import contracts
|
|
1989
|
+
contract = contracts.get(task_id)
|
|
1990
|
+
if ((contract and contract.get("approval_required")) or task.get("approval_required")) \
|
|
1991
|
+
and not contracts.release_allowed(task_id):
|
|
1992
|
+
return self._json(409, {"error": "任务需要先完成审批,才能发布产物",
|
|
1993
|
+
"contract": contract})
|
|
1818
1994
|
if op == "create-book":
|
|
1819
1995
|
kwargs = ({"force": force, "force_confirmed": force_confirmed,
|
|
1820
1996
|
"force_reason": force_reason}
|
|
@@ -1834,6 +2010,13 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
1834
2010
|
if not ap:
|
|
1835
2011
|
return self._json(400, {"error": ap_err})
|
|
1836
2012
|
ap["enabled"] = False
|
|
2013
|
+
if ap.get("enabled") and ap.get("auto_submit"):
|
|
2014
|
+
from core import contracts
|
|
2015
|
+
contract = contracts.get(task_id)
|
|
2016
|
+
if ((contract and contract.get("approval_required")) or
|
|
2017
|
+
task.get("approval_required")) and not contracts.release_allowed(task_id):
|
|
2018
|
+
return self._json(409, {"error": "任务需要先完成审批,才能启用自动提交",
|
|
2019
|
+
"contract": contract})
|
|
1837
2020
|
ok, err = store.set_auto_publish(task_id, ap), ""
|
|
1838
2021
|
if ok:
|
|
1839
2022
|
return self._json(200, {"ok": True, "auto_publish": ap})
|
|
@@ -3094,36 +3277,10 @@ def main():
|
|
|
3094
3277
|
from core import notify as _notify
|
|
3095
3278
|
usage_ledger.set_alert_push(_notify.push_text) # 花费预警推送注入(防 usage→notify 静态环)
|
|
3096
3279
|
from core import mcp_client as _mcp
|
|
3280
|
+
from core.plugins import merged_servers_text as _plugins_mcp_text
|
|
3097
3281
|
def _mcp_servers_text():
|
|
3098
3282
|
"""settings 清单 + 已启用插件的 MCP 服务器合成(plugin 名隔离防撞名)。"""
|
|
3099
|
-
|
|
3100
|
-
try:
|
|
3101
|
-
raw = str(settings.load().get("mcp_servers") or "")
|
|
3102
|
-
configured = _json.loads(raw) if raw.strip() else []
|
|
3103
|
-
except Exception:
|
|
3104
|
-
return raw or ""
|
|
3105
|
-
if not isinstance(configured, list):
|
|
3106
|
-
return raw or ""
|
|
3107
|
-
try:
|
|
3108
|
-
from core import plugins as _plugins
|
|
3109
|
-
plugin_servers = _plugins.active_mcp_servers()
|
|
3110
|
-
except Exception:
|
|
3111
|
-
plugin_servers = [] # 坏插件不能连累用户显式配置的服务器
|
|
3112
|
-
import hashlib as _hashlib
|
|
3113
|
-
import re as _re
|
|
3114
|
-
for item in plugin_servers or []:
|
|
3115
|
-
if not isinstance(item, dict):
|
|
3116
|
-
continue
|
|
3117
|
-
plugin_id = str(item.pop("plugin_id", "plugin"))
|
|
3118
|
-
server_name = str(item.get("name") or "server")
|
|
3119
|
-
digest = _hashlib.sha256(
|
|
3120
|
-
(plugin_id + ":" + server_name).encode("utf-8")).hexdigest()[:6]
|
|
3121
|
-
safe = _re.sub(r"[^a-z0-9_-]", "-", plugin_id.lower())
|
|
3122
|
-
item["name"] = ("plg_" + safe[:11] + "_" + digest)[:24]
|
|
3123
|
-
if not any(x.get("name") == item["name"] for x in configured
|
|
3124
|
-
if isinstance(x, dict)):
|
|
3125
|
-
configured.append(item)
|
|
3126
|
-
return _json.dumps(configured, ensure_ascii=False)
|
|
3283
|
+
return _plugins_mcp_text(settings.load().get("mcp_servers"))
|
|
3127
3284
|
_mcp.set_settings_text(_mcp_servers_text) # MCP 清单注入(防 mcp_client→settings 边)
|
|
3128
3285
|
_step("正在回填用量台账…")
|
|
3129
3286
|
n_bf = usage.backfill_from_runs() # 历史运行 token 回填台账(幂等,仅补缺失步骤)
|
package/app/mcp_server.py
CHANGED
|
@@ -25,7 +25,9 @@ TOOLS = [
|
|
|
25
25
|
"goal": {"type": "string", "description": "任务目标(一句话说清要做什么)"},
|
|
26
26
|
"type": {"type": "string", "description": "任务类型 id(如 code/novel/doc/direct),默认 doc"},
|
|
27
27
|
"workdir": {"type": "string", "description": "工作目录绝对路径;留空用 CodeBee 默认保存路径"},
|
|
28
|
-
"title": {"type": "string", "description": "任务标题;留空取目标前 30 字"}
|
|
28
|
+
"title": {"type": "string", "description": "任务标题;留空取目标前 30 字"},
|
|
29
|
+
"acceptance_criteria": {"type": "array", "items": {"type": "string"}},
|
|
30
|
+
"approval_required": {"type": "boolean"}},
|
|
29
31
|
"required": ["goal"]}},
|
|
30
32
|
{"name": "get_status", "description": "查任务与最近一次运行的状态:进度、当前步骤、评分、错误。",
|
|
31
33
|
"inputSchema": {"type": "object", "properties": {
|
|
@@ -38,6 +40,17 @@ TOOLS = [
|
|
|
38
40
|
{"name": "bench_leaderboard", "description": "模型评测基准台能力榜(综合分 Top N)。",
|
|
39
41
|
"inputSchema": {"type": "object", "properties": {
|
|
40
42
|
"top": {"type": "integer", "description": "取前 N,默认 5"}}}},
|
|
43
|
+
{"name": "get_contract", "description": "读取任务验收标准、证据、审批和完成回执。",
|
|
44
|
+
"inputSchema": {"type": "object", "properties": {
|
|
45
|
+
"task_id": {"type": "string"}}, "required": ["task_id"]}},
|
|
46
|
+
{"name": "record_evidence", "description": "向任务契约追加结构化验收证据。",
|
|
47
|
+
"inputSchema": {"type": "object", "properties": {
|
|
48
|
+
"task_id": {"type": "string"}, "criterion": {"type": "string"},
|
|
49
|
+
"status": {"type": "string", "enum": ["passed", "failed", "unknown", "not_evaluated"]},
|
|
50
|
+
"summary": {"type": "string"}}, "required": ["task_id", "status"]}},
|
|
51
|
+
{"name": "search_knowledge", "description": "在本地检索索引中搜索带来源和命中词解释的上下文。",
|
|
52
|
+
"inputSchema": {"type": "object", "properties": {
|
|
53
|
+
"query": {"type": "string"}, "limit": {"type": "integer"}}, "required": ["query"]}},
|
|
41
54
|
]
|
|
42
55
|
|
|
43
56
|
|
|
@@ -57,10 +70,38 @@ def _tool_create_task(args):
|
|
|
57
70
|
"goal": goal[:4000],
|
|
58
71
|
"workdir": str(args.get("workdir") or "").strip()
|
|
59
72
|
or settings_mod.default_workdir()}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
73
|
+
criteria = args.get("acceptance_criteria")
|
|
74
|
+
if criteria is not None and not isinstance(criteria, list):
|
|
75
|
+
return {"isError": True, "content": "acceptance_criteria 必须是列表"}
|
|
76
|
+
if isinstance(criteria, list):
|
|
77
|
+
payload["acceptance_criteria"] = [str(x).strip()[:500] for x in criteria if str(x).strip()][:40]
|
|
78
|
+
approval_required = args.get("approval_required", False)
|
|
79
|
+
if not isinstance(approval_required, bool):
|
|
80
|
+
return {"isError": True, "content": "approval_required 必须是布尔值"}
|
|
81
|
+
payload["approval_required"] = approval_required
|
|
82
|
+
try:
|
|
83
|
+
task = store.create_task(payload)
|
|
84
|
+
except ValueError as exc:
|
|
85
|
+
return {"isError": True, "content": str(exc)}
|
|
86
|
+
run = None
|
|
87
|
+
try:
|
|
88
|
+
run = store.create_run("orchestration", task["title"], task_id=task["id"])
|
|
89
|
+
store.update_task_status(task["id"], "queued")
|
|
90
|
+
_enqueue({"kind": "orchestration", "run_id": run["id"], "task_id": task["id"]})
|
|
91
|
+
except Exception as exc:
|
|
92
|
+
# Mirror the HTTP creator: every partially-created task/run is closed
|
|
93
|
+
# so MCP clients never receive a success while the UI shows queued.
|
|
94
|
+
if run:
|
|
95
|
+
try:
|
|
96
|
+
store.update_run(run["id"], status="failed", error="任务启动失败,请稍后重试",
|
|
97
|
+
ended_at=__import__("time").strftime("%Y-%m-%d %H:%M:%S"))
|
|
98
|
+
except Exception:
|
|
99
|
+
pass
|
|
100
|
+
try:
|
|
101
|
+
store.update_task_status(task["id"], "failed")
|
|
102
|
+
except Exception:
|
|
103
|
+
pass
|
|
104
|
+
return {"isError": True, "content": "任务启动失败,请稍后重试"}
|
|
64
105
|
return {"content": "已创建并排队:task_id=%s run_id=%s(可用 get_status 查进度)"
|
|
65
106
|
% (task["id"], run["id"])}
|
|
66
107
|
|
|
@@ -121,9 +162,37 @@ def _tool_bench(args):
|
|
|
121
162
|
return {"content": json.dumps(rows, ensure_ascii=False, indent=1)}
|
|
122
163
|
|
|
123
164
|
|
|
165
|
+
def _tool_contract(args):
|
|
166
|
+
from core import contracts
|
|
167
|
+
item = contracts.get(str(args.get("task_id") or "").strip())
|
|
168
|
+
if not item:
|
|
169
|
+
return {"isError": True, "content": "任务契约不存在"}
|
|
170
|
+
return {"content": json.dumps(item, ensure_ascii=False, indent=1)}
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _tool_evidence(args):
|
|
174
|
+
from core import contracts
|
|
175
|
+
try:
|
|
176
|
+
item = contracts.add_evidence(str(args.get("task_id") or "").strip(), args,
|
|
177
|
+
actor=str(args.get("actor") or "mcp")[:120])
|
|
178
|
+
except ValueError as exc:
|
|
179
|
+
return {"isError": True, "content": str(exc)}
|
|
180
|
+
return {"content": json.dumps(item, ensure_ascii=False, indent=1)}
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _tool_search(args):
|
|
184
|
+
from core import retrieval
|
|
185
|
+
try:
|
|
186
|
+
rows = retrieval.search(args.get("query"), args.get("limit") or 10)
|
|
187
|
+
except (TypeError, ValueError) as exc:
|
|
188
|
+
return {"isError": True, "content": str(exc)}
|
|
189
|
+
return {"content": json.dumps(rows, ensure_ascii=False, indent=1)}
|
|
190
|
+
|
|
191
|
+
|
|
124
192
|
_TOOL_IMPL = {"create_task": _tool_create_task, "get_status": _tool_get_status,
|
|
125
193
|
"list_recent": _tool_list_recent, "usage_summary": _tool_usage_summary,
|
|
126
|
-
"bench_leaderboard": _tool_bench
|
|
194
|
+
"bench_leaderboard": _tool_bench, "get_contract": _tool_contract,
|
|
195
|
+
"record_evidence": _tool_evidence, "search_knowledge": _tool_search}
|
|
127
196
|
|
|
128
197
|
|
|
129
198
|
def handle_request(msg):
|