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/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
- 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)})
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
- 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))})
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
- 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)
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.inherited_book_meta(task, p)
623
+ p: store.chain_book_meta(task, p)
571
624
  for p in ("fanqie", "qimao")
572
- if store.inherited_book_meta(task, p)}})
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
- if not bookmeta.needs_book_meta(task):
1786
- # 生成权留在首批(一本书一份资料):指路根任务,别让用户在续写批次上撞墙
1787
- root = next((store.get_task(t) for t
1788
- in store.serial_chain_ids(task_id) if t != task_id), None)
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 = ((task.get("book_meta") or {}).get(platform) or {})
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(task_id, platform,
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(task)
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=(task_id, platform, author)).start()
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
- 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):