codebee 0.1.73 → 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 +6 -0
- package/README.md +3 -3
- 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/health.py +14 -1
- package/app/core/jobs.py +50 -0
- package/app/core/manager.py +38 -14
- package/app/core/modelhub.py +104 -9
- package/app/core/openapi.py +154 -0
- package/app/core/pipeline.py +41 -0
- package/app/core/presence.py +77 -0
- package/app/core/publish/auto.py +11 -0
- package/app/core/retrieval.py +137 -0
- package/app/core/runner.py +85 -2
- package/app/core/store.py +72 -0
- package/app/core/story_tracking.py +241 -0
- package/app/main.py +181 -20
- package/app/mcp_server.py +75 -6
- package/app/ui/app.js +129 -31
- package/app/ui/i18n.js +19 -5
- package/app/ui/index.html +5 -0
- package/app/ui/style.css +10 -0
- 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),
|
|
@@ -311,6 +326,19 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
311
326
|
if path == "/api/knowledge":
|
|
312
327
|
from core import knowledge
|
|
313
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})
|
|
314
342
|
if path == "/api/settings":
|
|
315
343
|
return self._json(200, dict(settings.load(), **jobs.workers_info()))
|
|
316
344
|
if path == "/api/settings-v2":
|
|
@@ -537,7 +565,26 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
537
565
|
m = re.match(r"^/api/tasks/([^/]+)/detail$", path)
|
|
538
566
|
if m:
|
|
539
567
|
task = store.get_task(m.group(1))
|
|
540
|
-
|
|
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)})
|
|
541
588
|
m = re.match(r"^/api/tasks/([^/]+)/runs$", path)
|
|
542
589
|
if m:
|
|
543
590
|
# 任务级详情用:该任务全部 run(含 steps),不受前端 run 窗口限制
|
|
@@ -545,13 +592,18 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
545
592
|
return self._json(404, {"error": "not found"})
|
|
546
593
|
query = parse_qs(urlparse(self.path).query)
|
|
547
594
|
if "limit" not in query:
|
|
548
|
-
|
|
595
|
+
from core import contracts
|
|
596
|
+
return self._json(200, {"runs": store.task_runs(m.group(1)),
|
|
597
|
+
"contract": contracts.get(m.group(1))})
|
|
549
598
|
try:
|
|
550
599
|
limit = max(1, min(20, int((query.get("limit") or [3])[0])))
|
|
551
600
|
offset = max(0, int((query.get("offset") or [0])[0]))
|
|
552
601
|
except (TypeError, ValueError):
|
|
553
602
|
return self._json(400, {"error": "invalid pagination"})
|
|
554
|
-
|
|
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)
|
|
555
607
|
m = re.match(r"^/api/tasks/([^/]+)/bible$", path)
|
|
556
608
|
if m:
|
|
557
609
|
# 故事圣经:查看(无令牌豁免走 _authed 已过;本机免令牌)
|
|
@@ -562,14 +614,15 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
562
614
|
m = re.match(r"^/api/tasks/([^/]+)/book-meta$", path)
|
|
563
615
|
if m:
|
|
564
616
|
# 作品信息(番茄/七猫建书表单资料):读取生成状态与结果。
|
|
565
|
-
#
|
|
617
|
+
# 生成可从链上任意任务触发、落点在根任务(都是用第一个),
|
|
618
|
+
# running/failed 状态也要沿链可见(chain_book_meta)。
|
|
566
619
|
task = store.get_task(m.group(1))
|
|
567
620
|
if not task:
|
|
568
621
|
return self._json(404, {"error": "not found"})
|
|
569
622
|
return self._json(200, {"book_meta": {
|
|
570
|
-
p: store.
|
|
623
|
+
p: store.chain_book_meta(task, p)
|
|
571
624
|
for p in ("fanqie", "qimao")
|
|
572
|
-
if store.
|
|
625
|
+
if store.chain_book_meta(task, p)}})
|
|
573
626
|
if path == "/api/publish":
|
|
574
627
|
# 一键发布:平台连接状态 + 最近台账(详情页发布面板)
|
|
575
628
|
from core.publish import manager as pub
|
|
@@ -763,6 +816,9 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
763
816
|
return deny
|
|
764
817
|
if path == "/api/tasks":
|
|
765
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})
|
|
766
822
|
if path == "/api/hooks/save":
|
|
767
823
|
# 生命周期钩子配置整表保存(本机设置)
|
|
768
824
|
from core import hooks
|
|
@@ -780,6 +836,87 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
780
836
|
return self._json(200, {"ok": ok, "output": out})
|
|
781
837
|
if path == "/api/tasks/clarify":
|
|
782
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"))})
|
|
783
920
|
if path == "/api/notify/test":
|
|
784
921
|
# 设置页「发送测试」:向全部已配置推送通道发一条测试消息
|
|
785
922
|
from core import notify
|
|
@@ -1053,6 +1190,12 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
1053
1190
|
store.update_run(m.group(1), cancelled_by_user=True)
|
|
1054
1191
|
ok = jobs.cancel(m.group(1))
|
|
1055
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})
|
|
1056
1199
|
m = re.match(r"^/api/runs/([^/]+)/delete$", path)
|
|
1057
1200
|
if m:
|
|
1058
1201
|
ok, err = store.delete_run(m.group(1))
|
|
@@ -1711,6 +1854,13 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
1711
1854
|
task = store.get_task(task_id)
|
|
1712
1855
|
if not task:
|
|
1713
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})
|
|
1714
1864
|
if op == "git-merge":
|
|
1715
1865
|
from core import operations
|
|
1716
1866
|
operation_id = operations.begin(
|
|
@@ -1773,7 +1923,10 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
1773
1923
|
|
|
1774
1924
|
后台线程跑(编排者→作者CLI→模板的降级链可能数分钟),请求立即返回;
|
|
1775
1925
|
前端靠任务 book_meta 状态(SSE 全量状态里带)轮进度。已有 running 时
|
|
1776
|
-
|
|
1926
|
+
幂等拒绝,不重复起线程。生成权不限定首批(2026-09-30 实案:
|
|
1927
|
+
start_chapter>1 的独立任务被误拦,用户点首版也撞墙)——任何任务都能
|
|
1928
|
+
触发;但开书资料属于「这本书」,素材与落点统一取连载链根任务
|
|
1929
|
+
(都是用第一个),续写批次触发的生成整链可见、建书口径不分叉。"""
|
|
1777
1930
|
from core import bookmeta
|
|
1778
1931
|
task = store.get_task(task_id)
|
|
1779
1932
|
if not task:
|
|
@@ -1782,27 +1935,22 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
1782
1935
|
platform = (body.get("platform") or "").strip()
|
|
1783
1936
|
if platform not in bookmeta.PLATFORMS:
|
|
1784
1937
|
return self._json(400, {"error": "platform 必须是 fanqie 或 qimao"})
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
return self._json(400, {"error": (
|
|
1790
|
-
"开书资料在连载首批任务《%s》上生成与维护,请到首批任务的「作品信息」页操作"
|
|
1791
|
-
% ((root or {}).get("title") or "首批")
|
|
1792
|
-
if root else "只有连载首批任务需要作品信息;续写批次沿用第一批的开书资料")})
|
|
1793
|
-
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"):
|
|
1794
1942
|
return self._json(400, {"error": "任务正在运行,请等本轮结束后再生成作品信息"})
|
|
1795
|
-
cur = ((
|
|
1943
|
+
cur = ((target.get("book_meta") or {}).get(platform) or {})
|
|
1796
1944
|
if cur.get("status") == "running":
|
|
1797
1945
|
return self._json(200, {"ok": True, "already": True})
|
|
1798
|
-
if not store.set_book_meta(
|
|
1946
|
+
if not store.set_book_meta(target_id, platform,
|
|
1799
1947
|
{"status": "running",
|
|
1800
1948
|
"at": time.strftime("%Y-%m-%d %H:%M:%S")}):
|
|
1801
1949
|
return self._json(404, {"error": "任务不存在"})
|
|
1802
|
-
author = bookmeta._resolve_author(
|
|
1950
|
+
author = bookmeta._resolve_author(target)
|
|
1803
1951
|
threading.Thread(target=bookmeta.generate_async, daemon=True,
|
|
1804
1952
|
name="book-meta-%s" % platform,
|
|
1805
|
-
args=(
|
|
1953
|
+
args=(target_id, platform, author)).start()
|
|
1806
1954
|
return self._json(200, {"ok": True, "started": True})
|
|
1807
1955
|
|
|
1808
1956
|
def _api_publish_platform_op(self, platform, op):
|
|
@@ -1837,6 +1985,12 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
1837
1985
|
force = bool(body.get("force"))
|
|
1838
1986
|
force_confirmed = bool(body.get("force_confirmed"))
|
|
1839
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})
|
|
1840
1994
|
if op == "create-book":
|
|
1841
1995
|
kwargs = ({"force": force, "force_confirmed": force_confirmed,
|
|
1842
1996
|
"force_reason": force_reason}
|
|
@@ -1856,6 +2010,13 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
1856
2010
|
if not ap:
|
|
1857
2011
|
return self._json(400, {"error": ap_err})
|
|
1858
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})
|
|
1859
2020
|
ok, err = store.set_auto_publish(task_id, ap), ""
|
|
1860
2021
|
if ok:
|
|
1861
2022
|
return self._json(200, {"ok": True, "auto_publish": ap})
|
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):
|