codebee 0.1.73 → 0.1.76
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 +23 -0
- package/README.md +5 -4
- package/app/core/analytics.py +38 -0
- package/app/core/bookmeta.py +115 -14
- package/app/core/contracts.py +255 -0
- package/app/core/error_codes.py +9 -0
- package/app/core/health.py +16 -1
- package/app/core/jobs.py +72 -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 +210 -18
- 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 +101 -5
- package/app/core/store.py +72 -0
- package/app/core/story_tracking.py +275 -0
- package/app/main.py +240 -20
- package/app/mcp_server.py +75 -6
- package/app/ui/app.js +237 -43
- package/app/ui/i18n.js +34 -5
- package/app/ui/index.html +9 -0
- package/app/ui/style.css +32 -6
- 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
|
|
@@ -959,6 +1096,9 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
959
1096
|
m = re.match(r"^/api/tasks/([^/]+)/book-meta$", path)
|
|
960
1097
|
if m:
|
|
961
1098
|
return self._api_book_meta_generate(m.group(1))
|
|
1099
|
+
m = re.match(r"^/api/tasks/([^/]+)/book-meta/rename$", path)
|
|
1100
|
+
if m:
|
|
1101
|
+
return self._api_book_meta_rename(m.group(1))
|
|
962
1102
|
m = re.match(r"^/api/tasks/([^/]+)/cover$", path)
|
|
963
1103
|
if m:
|
|
964
1104
|
from core import covergen
|
|
@@ -1053,6 +1193,12 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
1053
1193
|
store.update_run(m.group(1), cancelled_by_user=True)
|
|
1054
1194
|
ok = jobs.cancel(m.group(1))
|
|
1055
1195
|
return self._json(200, {"ok": ok})
|
|
1196
|
+
m = re.match(r"^/api/runs/([^/]+)/resume_now$", path)
|
|
1197
|
+
if m:
|
|
1198
|
+
# 立即重试:跳过自动续跑的退避等待马上入队(用户点按钮 = 主动
|
|
1199
|
+
# 选择不等退避窗口)。设备控制权由 do_POST 顶部的统一写闸把关。
|
|
1200
|
+
ok, err = jobs.enqueue_now(m.group(1))
|
|
1201
|
+
return self._json(400, {"error": err}) if not ok else self._json(200, {"ok": True})
|
|
1056
1202
|
m = re.match(r"^/api/runs/([^/]+)/delete$", path)
|
|
1057
1203
|
if m:
|
|
1058
1204
|
ok, err = store.delete_run(m.group(1))
|
|
@@ -1062,6 +1208,10 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
1062
1208
|
if err and not n:
|
|
1063
1209
|
return self._json(400, {"error": err})
|
|
1064
1210
|
return self._json(200, {"ok": True, "count": n, "skipped": skipped, "message": err})
|
|
1211
|
+
if path == "/api/runs/cancel_all":
|
|
1212
|
+
# 急停(kill switch):取消全部进行中任务。全量扫描不受侧栏
|
|
1213
|
+
# 窗口限制——窗口外的活跃运行也一并取消(hcom/amux 族竞品标配)。
|
|
1214
|
+
return self._json(200, {"ok": True, "count": jobs.cancel_all()})
|
|
1065
1215
|
if path == "/api/runs/clear":
|
|
1066
1216
|
n, skipped = store.clear_runs()
|
|
1067
1217
|
return self._json(200, {"ok": True, "count": n, "skipped": skipped})
|
|
@@ -1711,6 +1861,13 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
1711
1861
|
task = store.get_task(task_id)
|
|
1712
1862
|
if not task:
|
|
1713
1863
|
return self._json(404, {"error": "任务不存在"})
|
|
1864
|
+
if op == "git-merge":
|
|
1865
|
+
from core import contracts
|
|
1866
|
+
contract = contracts.get(task_id)
|
|
1867
|
+
if (contract and contract.get("approval_required")) or task.get("approval_required"):
|
|
1868
|
+
if not contracts.release_allowed(task_id):
|
|
1869
|
+
return self._json(409, {"error": "任务需要先完成审批,才能合并产物",
|
|
1870
|
+
"contract": contract})
|
|
1714
1871
|
if op == "git-merge":
|
|
1715
1872
|
from core import operations
|
|
1716
1873
|
operation_id = operations.begin(
|
|
@@ -1773,7 +1930,10 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
1773
1930
|
|
|
1774
1931
|
后台线程跑(编排者→作者CLI→模板的降级链可能数分钟),请求立即返回;
|
|
1775
1932
|
前端靠任务 book_meta 状态(SSE 全量状态里带)轮进度。已有 running 时
|
|
1776
|
-
|
|
1933
|
+
幂等拒绝,不重复起线程。生成权不限定首批(2026-09-30 实案:
|
|
1934
|
+
start_chapter>1 的独立任务被误拦,用户点首版也撞墙)——任何任务都能
|
|
1935
|
+
触发;但开书资料属于「这本书」,素材与落点统一取连载链根任务
|
|
1936
|
+
(都是用第一个),续写批次触发的生成整链可见、建书口径不分叉。"""
|
|
1777
1937
|
from core import bookmeta
|
|
1778
1938
|
task = store.get_task(task_id)
|
|
1779
1939
|
if not task:
|
|
@@ -1782,29 +1942,76 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
1782
1942
|
platform = (body.get("platform") or "").strip()
|
|
1783
1943
|
if platform not in bookmeta.PLATFORMS:
|
|
1784
1944
|
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"):
|
|
1945
|
+
chain = store.serial_chain_ids(task_id)
|
|
1946
|
+
target = (store.get_task(chain[0]) if chain else None) or task
|
|
1947
|
+
target_id = str(target.get("id"))
|
|
1948
|
+
if target.get("status") in ("queued", "running"):
|
|
1794
1949
|
return self._json(400, {"error": "任务正在运行,请等本轮结束后再生成作品信息"})
|
|
1795
|
-
cur = ((
|
|
1950
|
+
cur = ((target.get("book_meta") or {}).get(platform) or {})
|
|
1796
1951
|
if cur.get("status") == "running":
|
|
1797
1952
|
return self._json(200, {"ok": True, "already": True})
|
|
1798
|
-
if not store.set_book_meta(
|
|
1953
|
+
if not store.set_book_meta(target_id, platform,
|
|
1799
1954
|
{"status": "running",
|
|
1800
1955
|
"at": time.strftime("%Y-%m-%d %H:%M:%S")}):
|
|
1801
1956
|
return self._json(404, {"error": "任务不存在"})
|
|
1802
|
-
author = bookmeta._resolve_author(
|
|
1957
|
+
author = bookmeta._resolve_author(target)
|
|
1803
1958
|
threading.Thread(target=bookmeta.generate_async, daemon=True,
|
|
1804
1959
|
name="book-meta-%s" % platform,
|
|
1805
|
-
args=(
|
|
1960
|
+
args=(target_id, platform, author)).start()
|
|
1806
1961
|
return self._json(200, {"ok": True, "started": True})
|
|
1807
1962
|
|
|
1963
|
+
def _api_book_meta_rename(self, task_id):
|
|
1964
|
+
"""作品信息换名(POST /api/tasks/<id>/book-meta/rename,body: {platform})。
|
|
1965
|
+
|
|
1966
|
+
只换 book_name 一个字段:整套重生成会覆盖已核对过的分类/标签/简介,
|
|
1967
|
+
而用户的痛点只是书名与本站已有作品撞名。同步接口(单次轻模型调用),
|
|
1968
|
+
成功才写盘,模型失败时现有数据原样不动。排除名单=全站已用书名
|
|
1969
|
+
(books.json 登记 + 各任务 book_meta),从根上防「换出来的还是重名」。
|
|
1970
|
+
落点同生成:连载链根任务(2026-09-30 都是用第一个)。"""
|
|
1971
|
+
from core import bookmeta
|
|
1972
|
+
task = store.get_task(task_id)
|
|
1973
|
+
if not task:
|
|
1974
|
+
return self._json(404, {"error": "任务不存在"})
|
|
1975
|
+
body = self._body() or {}
|
|
1976
|
+
platform = (body.get("platform") or "").strip()
|
|
1977
|
+
if platform not in bookmeta.PLATFORMS:
|
|
1978
|
+
return self._json(400, {"error": "platform 必须是 fanqie 或 qimao"})
|
|
1979
|
+
chain = store.serial_chain_ids(task_id)
|
|
1980
|
+
target = (store.get_task(chain[0]) if chain else None) or task
|
|
1981
|
+
target_id = str(target.get("id"))
|
|
1982
|
+
if target.get("status") in ("queued", "running"):
|
|
1983
|
+
return self._json(400, {"error": "任务正在运行,请等本轮结束后再换名"})
|
|
1984
|
+
entry = ((target.get("book_meta") or {}).get(platform) or {})
|
|
1985
|
+
if entry.get("status") != "done" or not isinstance(entry.get("data"), dict):
|
|
1986
|
+
return self._json(400, {"error": "该平台作品信息尚未生成,没有书名可换"})
|
|
1987
|
+
old_name = str((entry["data"] or {}).get("book_name") or "").strip()
|
|
1988
|
+
if not old_name:
|
|
1989
|
+
return self._json(400, {"error": "现有记录没有书名可换"})
|
|
1990
|
+
new_name, err = bookmeta.rename_book(target, platform, old_name,
|
|
1991
|
+
bookmeta.used_book_names())
|
|
1992
|
+
if err:
|
|
1993
|
+
return self._json(400, {"error": err})
|
|
1994
|
+
data = dict(entry["data"])
|
|
1995
|
+
data["book_name"] = new_name
|
|
1996
|
+
payload = dict(entry)
|
|
1997
|
+
payload["data"] = data
|
|
1998
|
+
payload["at"] = time.strftime("%Y-%m-%d %H:%M:%S")
|
|
1999
|
+
payload["source"] = (str(entry.get("source") or "") +
|
|
2000
|
+
";换名:《%s》→《%s》" % (old_name, new_name)).lstrip(";")
|
|
2001
|
+
if not store.set_book_meta(target_id, platform, payload):
|
|
2002
|
+
return self._json(404, {"error": "任务不存在"})
|
|
2003
|
+
# 归档 Markdown 同步(工作目录在场才写;source 只进归档不进 book_meta
|
|
2004
|
+
# 数据——数据里多的键会让 repair_existing 的幂等比较每次不等)
|
|
2005
|
+
try:
|
|
2006
|
+
fp = Path(target.get("workdir") or "") / bookmeta.PLATFORMS[platform]["file"]
|
|
2007
|
+
if fp.parent.is_dir():
|
|
2008
|
+
fp.write_text(bookmeta.render_markdown(
|
|
2009
|
+
target, platform, dict(data, source=payload["source"])),
|
|
2010
|
+
encoding="utf-8")
|
|
2011
|
+
except OSError:
|
|
2012
|
+
pass
|
|
2013
|
+
return self._json(200, {"ok": True, "old": old_name, "new": new_name})
|
|
2014
|
+
|
|
1808
2015
|
def _api_publish_platform_op(self, platform, op):
|
|
1809
2016
|
"""平台会话操作:connect 开浏览器等扫码 / disconnect 关 / probe 探测表单。"""
|
|
1810
2017
|
from core.publish import manager as pub
|
|
@@ -1837,6 +2044,12 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
1837
2044
|
force = bool(body.get("force"))
|
|
1838
2045
|
force_confirmed = bool(body.get("force_confirmed"))
|
|
1839
2046
|
force_reason = str(body.get("force_reason") or "").strip()
|
|
2047
|
+
from core import contracts
|
|
2048
|
+
contract = contracts.get(task_id)
|
|
2049
|
+
if ((contract and contract.get("approval_required")) or task.get("approval_required")) \
|
|
2050
|
+
and not contracts.release_allowed(task_id):
|
|
2051
|
+
return self._json(409, {"error": "任务需要先完成审批,才能发布产物",
|
|
2052
|
+
"contract": contract})
|
|
1840
2053
|
if op == "create-book":
|
|
1841
2054
|
kwargs = ({"force": force, "force_confirmed": force_confirmed,
|
|
1842
2055
|
"force_reason": force_reason}
|
|
@@ -1856,6 +2069,13 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
1856
2069
|
if not ap:
|
|
1857
2070
|
return self._json(400, {"error": ap_err})
|
|
1858
2071
|
ap["enabled"] = False
|
|
2072
|
+
if ap.get("enabled") and ap.get("auto_submit"):
|
|
2073
|
+
from core import contracts
|
|
2074
|
+
contract = contracts.get(task_id)
|
|
2075
|
+
if ((contract and contract.get("approval_required")) or
|
|
2076
|
+
task.get("approval_required")) and not contracts.release_allowed(task_id):
|
|
2077
|
+
return self._json(409, {"error": "任务需要先完成审批,才能启用自动提交",
|
|
2078
|
+
"contract": contract})
|
|
1859
2079
|
ok, err = store.set_auto_publish(task_id, ap), ""
|
|
1860
2080
|
if ok:
|
|
1861
2081
|
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):
|