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/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
- return self._json(200, {"task": task}) if task else self._json(404, {"error": "not found"})
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
- return self._json(200, {"runs": store.task_runs(m.group(1))})
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
- return self._json(200, store.task_runs_page(m.group(1), offset, limit))
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.inherited_book_meta(task, p)
623
+ p: store.chain_book_meta(task, p)
566
624
  for p in ("fanqie", "qimao")
567
- if store.inherited_book_meta(task, p)}})
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
- if not bookmeta.needs_book_meta(task):
1764
- # 生成权留在首批(一本书一份资料):指路根任务,别让用户在续写批次上撞墙
1765
- root = next((store.get_task(t) for t
1766
- in store.serial_chain_ids(task_id) if t != task_id), None)
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 = ((task.get("book_meta") or {}).get(platform) or {})
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(task_id, platform,
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(task)
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=(task_id, platform, author)).start()
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
- import json as _json
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
- task = store.create_task(payload)
61
- run = store.create_run("orchestration", task["title"], task_id=task["id"])
62
- store.update_task_status(task["id"], "queued")
63
- _enqueue({"kind": "orchestration", "run_id": run["id"], "task_id": task["id"]})
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):