codebee 0.1.71 → 0.1.72

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.
Files changed (115) hide show
  1. package/CHANGELOG.md +595 -589
  2. package/LICENSE +21 -21
  3. package/README.md +508 -509
  4. package/app/core/acceptance.py +100 -100
  5. package/app/core/aiflavor.py +197 -197
  6. package/app/core/artifacts.py +148 -148
  7. package/app/core/assets.py +110 -110
  8. package/app/core/attachments.py +623 -623
  9. package/app/core/audit.py +30 -30
  10. package/app/core/backup.py +470 -470
  11. package/app/core/beekeeper.py +183 -183
  12. package/app/core/benchstore.py +95 -95
  13. package/app/core/board.py +209 -209
  14. package/app/core/bookmeta.py +935 -935
  15. package/app/core/bookmeta_catalog.py +180 -180
  16. package/app/core/branching.py +137 -137
  17. package/app/core/builtin_agent.py +1680 -1673
  18. package/app/core/capability.py +144 -144
  19. package/app/core/chaptersafe.py +32 -32
  20. package/app/core/cleanup.py +303 -303
  21. package/app/core/compaction.py +201 -201
  22. package/app/core/covergen.py +356 -356
  23. package/app/core/defectretro.py +43 -43
  24. package/app/core/diagnostics.py +114 -114
  25. package/app/core/dispatch.py +190 -190
  26. package/app/core/dispatch_log.py +128 -128
  27. package/app/core/env_scrub.py +85 -85
  28. package/app/core/errorlog.py +137 -137
  29. package/app/core/evalbench.py +836 -720
  30. package/app/core/evaluation.py +174 -174
  31. package/app/core/failure_notify.py +290 -290
  32. package/app/core/flows.py +7 -7
  33. package/app/core/goal_service.py +167 -167
  34. package/app/core/health.py +374 -374
  35. package/app/core/history.py +38 -38
  36. package/app/core/hooks.py +162 -162
  37. package/app/core/jobs.py +21 -0
  38. package/app/core/manager.py +2143 -2143
  39. package/app/core/market.py +88 -2
  40. package/app/core/market_remote.py +16 -0
  41. package/app/core/mocks.py +64 -64
  42. package/app/core/modelhub.py +3 -2
  43. package/app/core/notify.py +201 -201
  44. package/app/core/novel_quality.py +189 -189
  45. package/app/core/operations.py +228 -228
  46. package/app/core/paihang.py +138 -138
  47. package/app/core/pipeline.py +102 -30
  48. package/app/core/portguard.py +111 -111
  49. package/app/core/preview.py +293 -293
  50. package/app/core/prompt_guard.py +78 -0
  51. package/app/core/publish/__init__.py +7 -7
  52. package/app/core/publish/auto.py +396 -367
  53. package/app/core/publish/browser.py +655 -655
  54. package/app/core/publish/fanqie.py +165 -165
  55. package/app/core/publish/flow.py +589 -589
  56. package/app/core/publish/flows-fanqie-calibrated.json +336 -336
  57. package/app/core/publish/ledger.py +307 -229
  58. package/app/core/publish/manager.py +931 -824
  59. package/app/core/publish/qimao.py +146 -146
  60. package/app/core/publish/ws.py +139 -139
  61. package/app/core/quality_gate.py +194 -0
  62. package/app/core/redact.py +38 -38
  63. package/app/core/repeat_guard.py +129 -129
  64. package/app/core/router.py +398 -398
  65. package/app/core/runner.py +2142 -2142
  66. package/app/core/selfupdate.py +384 -384
  67. package/app/core/session_log.py +161 -161
  68. package/app/core/sessions.py +312 -312
  69. package/app/core/settings_schema.py +270 -270
  70. package/app/core/share_page.py +114 -114
  71. package/app/core/skill_scan.py +139 -122
  72. package/app/core/skillpacks/fanqie-novel.md +80 -80
  73. package/app/core/skillpacks/market/character-bible.md +66 -66
  74. package/app/core/skillpacks/market/code-risk-checklist.md +58 -58
  75. package/app/core/skillpacks/market/git-workflow.md +57 -57
  76. package/app/core/skillpacks/market/release-notes.md +72 -72
  77. package/app/core/skillpacks/market/weekly-report.md +71 -71
  78. package/app/core/skillpacks/market/worldview-consistency.md +70 -70
  79. package/app/core/skills.py +58 -8
  80. package/app/core/step_runner.py +132 -132
  81. package/app/core/store.py +51 -0
  82. package/app/core/task_compile.py +199 -199
  83. package/app/core/telemetry.py +292 -292
  84. package/app/core/tlsctx.py +64 -64
  85. package/app/core/token_meter.py +147 -147
  86. package/app/core/tracing.py +144 -144
  87. package/app/core/upstream.py +41 -41
  88. package/app/core/usage.py +1035 -1035
  89. package/app/core/volumes.py +389 -389
  90. package/app/core/wxdigest.py +858 -858
  91. package/app/core/zentao.py +2692 -2614
  92. package/app/main.py +86 -11
  93. package/app/mcp_server.py +175 -175
  94. package/app/pick_dialog.py +110 -110
  95. package/app/ui/app.js +258 -54
  96. package/app/ui/artdiff.js +74 -74
  97. package/app/ui/board.html +162 -162
  98. package/app/ui/board.js +305 -305
  99. package/app/ui/i18n.js +24 -0
  100. package/app/ui/icons/bee.svg +79 -79
  101. package/app/ui/index.html +5 -0
  102. package/app/ui/manifest.json +16 -16
  103. package/app/ui/qrcode.js +2297 -2297
  104. package/app/ui/style.css +7051 -7012
  105. package/bin/tutti.js +147 -147
  106. package/package.json +48 -47
  107. package/tools/wx_reader.py +261 -261
  108. package/app/.playwright-cli/console-2026-09-23T08-26-03-635Z.log +0 -88
  109. package/app/.playwright-cli/page-2026-09-23T08-26-03-845Z.yml +0 -126
  110. package/app/.playwright-cli/page-2026-09-23T08-29-44-203Z.png +0 -0
  111. package/app/.playwright-cli/page-2026-09-23T08-35-42-046Z.yml +0 -210
  112. package/app/.playwright-cli/page-2026-09-23T08-36-52-858Z.png +0 -0
  113. package/app/.playwright-cli/page-2026-09-23T08-37-33-938Z.png +0 -0
  114. package/app/.playwright-cli/run-detail-after.png +0 -0
  115. package/app//344/275/234/345/223/201/344/277/241/346/201/257-/344/270/203/347/214/253.md +0 -18
package/app/main.py CHANGED
@@ -287,7 +287,22 @@ class Handler(BaseHTTPRequestHandler):
287
287
  return self._json(200, {"versions": flows.flow_versions(fid)})
288
288
  if path == "/api/skills":
289
289
  from core import skills
290
- return self._json(200, skills.view())
290
+ v = skills.view()
291
+ try:
292
+ # 技能行为卡(ClawHub Skill Card 模式):市场安装的包按文件
293
+ # 名关联拼卡——声明/来源/指纹/安检一次可见。skills(L1) 不能
294
+ # 反向 import market(成环),拼接放路由层。
295
+ from core import market as _market
296
+ reg = _market._load_registry().get("installed") or {}
297
+ by_file = {str(r.get("file") or ""): pid
298
+ for pid, r in reg.items() if isinstance(r, dict)}
299
+ for p in v.get("packs") or []:
300
+ pid = by_file.get("market-%s.md" % p.get("id"))
301
+ if pid:
302
+ p["card"] = _market.skill_card(pid)
303
+ except Exception:
304
+ pass
305
+ return self._json(200, v)
291
306
  if path == "/api/knowledge":
292
307
  from core import knowledge
293
308
  return self._json(200, knowledge.view())
@@ -309,6 +324,14 @@ class Handler(BaseHTTPRequestHandler):
309
324
  if path == "/api/evalbench":
310
325
  from core import evalbench
311
326
  return self._json(200, evalbench.state())
327
+ if path == "/api/evalbench/report":
328
+ # 评测报告导出(Markdown,浏览器下载)
329
+ from core import evalbench
330
+ return self._send(200, evalbench.report_markdown().encode("utf-8"),
331
+ ctype="text/markdown; charset=utf-8",
332
+ headers={"Content-Disposition":
333
+ 'attachment; filename="codebee-bench-%s.md"'
334
+ % time.strftime("%Y%m%d-%H%M%S")})
312
335
  if path == "/api/catalog":
313
336
  return self._json(200, {"catalog": manager.catalog_view(),
314
337
  "checking": manager.updates_checking()})
@@ -533,11 +556,15 @@ class Handler(BaseHTTPRequestHandler):
533
556
  return self._json(200, {"path": fp, "text": text})
534
557
  m = re.match(r"^/api/tasks/([^/]+)/book-meta$", path)
535
558
  if m:
536
- # 作品信息(番茄/七猫建书表单资料):读取任务上的生成状态与结果
559
+ # 作品信息(番茄/七猫建书表单资料):读取生成状态与结果。
560
+ # 连载链沿链继承(都是用第一个):续写批次读到的就是首批的结果。
537
561
  task = store.get_task(m.group(1))
538
562
  if not task:
539
563
  return self._json(404, {"error": "not found"})
540
- return self._json(200, {"book_meta": task.get("book_meta") or {}})
564
+ return self._json(200, {"book_meta": {
565
+ p: store.inherited_book_meta(task, p)
566
+ for p in ("fanqie", "qimao")
567
+ if store.inherited_book_meta(task, p)}})
541
568
  if path == "/api/publish":
542
569
  # 一键发布:平台连接状态 + 最近台账(详情页发布面板)
543
570
  from core.publish import manager as pub
@@ -549,7 +576,8 @@ class Handler(BaseHTTPRequestHandler):
549
576
  from core.publish import manager as pub
550
577
  from core.publish import ledger as pub_ledger
551
578
  tid = m.group(1)
552
- books = pub_ledger.load_books().get(tid) or {}
579
+ # 绑定沿连载链继承(都是用第一个):续写批次也要看到这本书的登记
580
+ books = pub_ledger.books_for(tid)
553
581
  return self._json(200, {"history": pub.history(task_id=tid, limit=50),
554
582
  "books": books,
555
583
  "published": {p: pub.published_local(tid, p)
@@ -937,6 +965,15 @@ class Handler(BaseHTTPRequestHandler):
937
965
  m = re.match(r"^/api/publish/task/([^/]+)/(create-book|chapter|auto-publish)$", path)
938
966
  if m:
939
967
  return self._api_publish_task_op(m.group(1), m.group(2))
968
+ m = re.match(r"^/api/publish/task/([^/]+)/confirm-chapter$", path)
969
+ if m:
970
+ from core.publish import manager as pub
971
+ body = self._body() or {}
972
+ platform = (body.get("platform") or "").strip()
973
+ ok, err = pub.confirm_manual_chapter(
974
+ m.group(1), platform, body.get("chapter_no"))
975
+ return self._json(400, {"error": err or "确认失败"}) \
976
+ if not ok else self._json(200, {"ok": True})
940
977
  m = re.match(r"^/api/publish/task/([^/]+)/register-book$", path)
941
978
  if m:
942
979
  return self._api_publish_register_book(m.group(1))
@@ -960,7 +997,9 @@ class Handler(BaseHTTPRequestHandler):
960
997
  return self._json(404, {"error": "任务不存在"})
961
998
  body = self._body() or {}
962
999
  plat = str(body.get("platform") or "").strip()
963
- books = pub_ledger.load_books().get(tid) or {}
1000
+ # 绑定沿连载链继承(与 history 端点同口径):续写批次发起校准也能
1001
+ # 落到这本书的账上,否则前端看到继承绑定、这里却报「未登记」互相打架
1002
+ books = pub_ledger.books_for(tid)
964
1003
  if plat and plat not in books:
965
1004
  return self._json(400, {"error": "该平台未登记作品"})
966
1005
  plats = [plat] if plat else list(books)
@@ -1568,6 +1607,14 @@ class Handler(BaseHTTPRequestHandler):
1568
1607
  except zentao.ZenError as e:
1569
1608
  return self._json(404, {"error": str(e)})
1570
1609
  return self._json(200, {"ok": True, "claim": c})
1610
+ if path == "/api/zentao/claims/retry":
1611
+ from core import zentao
1612
+ body = self._body() or {}
1613
+ try:
1614
+ c, msg = zentao.retry_claim(body.get("bug_id"))
1615
+ except zentao.ZenError as e:
1616
+ return self._json(409, {"error": str(e)})
1617
+ return self._json(200, {"ok": True, "claim": c, "message": msg})
1571
1618
  if path == "/api/zentao/modules":
1572
1619
  from core import zentao
1573
1620
  pid = (self._body() or {}).get("product")
@@ -1714,7 +1761,13 @@ class Handler(BaseHTTPRequestHandler):
1714
1761
  if platform not in bookmeta.PLATFORMS:
1715
1762
  return self._json(400, {"error": "platform 必须是 fanqie 或 qimao"})
1716
1763
  if not bookmeta.needs_book_meta(task):
1717
- return self._json(400, {"error": "只有连载首批任务需要作品信息;续写批次沿用第一批的开书资料"})
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 "只有连载首批任务需要作品信息;续写批次沿用第一批的开书资料")})
1718
1771
  if task.get("status") in ("queued", "running"):
1719
1772
  return self._json(400, {"error": "任务正在运行,请等本轮结束后再生成作品信息"})
1720
1773
  cur = ((task.get("book_meta") or {}).get(platform) or {})
@@ -1759,8 +1812,14 @@ class Handler(BaseHTTPRequestHandler):
1759
1812
  if platform not in ("fanqie", "qimao"):
1760
1813
  return self._json(400, {"error": "platform 必须是 fanqie 或 qimao"})
1761
1814
  auto_submit = bool(body.get("auto_submit"))
1815
+ force = bool(body.get("force"))
1816
+ force_confirmed = bool(body.get("force_confirmed"))
1817
+ force_reason = str(body.get("force_reason") or "").strip()
1762
1818
  if op == "create-book":
1763
- ok, err = pub.create_book_async(task_id, platform, auto_submit)
1819
+ kwargs = ({"force": force, "force_confirmed": force_confirmed,
1820
+ "force_reason": force_reason}
1821
+ if force or force_confirmed or force_reason else {})
1822
+ ok, err = pub.create_book_async(task_id, platform, auto_submit, **kwargs)
1764
1823
  elif op == "auto-publish":
1765
1824
  # 定时发布配置(P2.5):enabled=false 也落(保留 time 供再开);
1766
1825
  # 校验/归一在 auto.norm_auto_publish,语义见 auto.py 头注
@@ -1789,9 +1848,16 @@ class Handler(BaseHTTPRequestHandler):
1789
1848
  fp.relative_to(_P(wd).resolve())
1790
1849
  except (OSError, ValueError):
1791
1850
  return self._json(400, {"error": "章节文件必须在任务工作目录内"})
1792
- ok, err = pub.upload_chapter_async(task_id, platform, str(fp), auto_submit)
1851
+ kwargs = ({"force": force, "force_confirmed": force_confirmed,
1852
+ "force_reason": force_reason}
1853
+ if force or force_confirmed or force_reason else {})
1854
+ ok, err = pub.upload_chapter_async(task_id, platform, str(fp), auto_submit,
1855
+ **kwargs)
1793
1856
  if not ok:
1794
- return self._json(400, {"error": err or "操作失败"})
1857
+ payload = {"error": err or "操作失败"}
1858
+ if str(err or "").startswith("质量门禁拦截:"):
1859
+ payload["quality_gate"] = {"status": "blocked", "message": err}
1860
+ return self._json(400, payload)
1795
1861
  return self._json(200, {"ok": True, "started": True})
1796
1862
 
1797
1863
  def _api_publish_register_book(self, task_id):
@@ -1821,10 +1887,19 @@ class Handler(BaseHTTPRequestHandler):
1821
1887
  platform = (body.get("platform") or "").strip()
1822
1888
  if platform not in ("fanqie", "qimao"):
1823
1889
  return self._json(400, {"error": "platform 必须是 fanqie 或 qimao"})
1890
+ force = bool(body.get("force"))
1891
+ force_confirmed = bool(body.get("force_confirmed"))
1892
+ force_reason = str(body.get("force_reason") or "").strip()
1824
1893
  ok, err = pub_auto.publish_pending_async(
1825
- task_id, platform, auto_submit=bool(body.get("auto_submit")))
1894
+ task_id, platform, auto_submit=bool(body.get("auto_submit")),
1895
+ **({"force": force, "force_confirmed": force_confirmed,
1896
+ "force_reason": force_reason}
1897
+ if force or force_confirmed or force_reason else {}))
1826
1898
  if not ok:
1827
- return self._json(400, {"error": err or "操作失败"})
1899
+ payload = {"error": err or "操作失败"}
1900
+ if str(err or "").startswith("质量门禁拦截:"):
1901
+ payload["quality_gate"] = {"status": "blocked", "message": err}
1902
+ return self._json(400, payload)
1828
1903
  return self._json(200, {"ok": True, "started": True})
1829
1904
 
1830
1905
  def _api_task_side(self, task_id):
package/app/mcp_server.py CHANGED
@@ -1,175 +1,175 @@
1
- # -*- coding: utf-8 -*-
2
- """CodeBee 的 MCP 服务器形态:其他 AI 客户端(Claude Code / ZCode / Cursor)
3
- 把这里当工具服务器挂载,即可直接创建任务、查状态、看用量与评测榜单——
4
- CodeBee 从编排工具变成可以被别的智能体调度的生态节点。
5
-
6
- stdio JSON-RPC 2.0(按行分帧,与 core/mcp_client 同一传输约定)。启动:
7
- python -X utf8 <CodeBee目录>/app/mcp_server.py
8
- 客户端配置示例(Claude Code / 兼容客户端):
9
- {"mcpServers": {"codebee": {"command": "python",
10
- "args": ["-X", "utf8", "<CodeBee目录>/app/mcp_server.py"]}}}
11
- 仅本机 stdio,无网络监听,凭据不经过此通道;create_task 会真实调度智能体
12
- 执行(与界面上建任务同一条链)。
13
- """
14
- from __future__ import annotations
15
-
16
- import json
17
- import sys
18
-
19
- PROTOCOL_VERSION = "2024-11-05"
20
- SERVER_INFO = {"name": "codebee", "version": "1.0"}
21
-
22
- TOOLS = [
23
- {"name": "create_task", "description": "创建并启动一个 CodeBee 编排任务(真实调度智能体执行,与界面建任务同链)。返回 task_id 与 run_id。",
24
- "inputSchema": {"type": "object", "properties": {
25
- "goal": {"type": "string", "description": "任务目标(一句话说清要做什么)"},
26
- "type": {"type": "string", "description": "任务类型 id(如 code/novel/doc/direct),默认 doc"},
27
- "workdir": {"type": "string", "description": "工作目录绝对路径;留空用 CodeBee 默认保存路径"},
28
- "title": {"type": "string", "description": "任务标题;留空取目标前 30 字"}},
29
- "required": ["goal"]}},
30
- {"name": "get_status", "description": "查任务与最近一次运行的状态:进度、当前步骤、评分、错误。",
31
- "inputSchema": {"type": "object", "properties": {
32
- "task_id": {"type": "string", "description": "任务 ID"}}, "required": ["task_id"]}},
33
- {"name": "list_recent", "description": "列出最近的运行(id/标题/状态/时间)。",
34
- "inputSchema": {"type": "object", "properties": {
35
- "limit": {"type": "integer", "description": "条数,默认 10,最大 30"}}}},
36
- {"name": "usage_summary", "description": "用量台账汇总:今日/本月花费(¥)与 token 总量。",
37
- "inputSchema": {"type": "object", "properties": {}}},
38
- {"name": "bench_leaderboard", "description": "模型评测基准台能力榜(综合分 Top N)。",
39
- "inputSchema": {"type": "object", "properties": {
40
- "top": {"type": "integer", "description": "取前 N,默认 5"}}}},
41
- ]
42
-
43
-
44
- def _enqueue(job):
45
- """thin wrapper:便于单测 mock(真链会拉起 worker 执行)。"""
46
- from core import jobs
47
- jobs.enqueue(job)
48
-
49
-
50
- def _tool_create_task(args):
51
- from core import settings as settings_mod, store
52
- goal = str(args.get("goal") or "").strip()
53
- if not goal:
54
- return {"isError": True, "content": "goal 不能为空"}
55
- payload = {"type": str(args.get("type") or "doc").strip()[:40] or "doc",
56
- "title": (str(args.get("title") or "").strip() or goal[:30])[:40],
57
- "goal": goal[:4000],
58
- "workdir": str(args.get("workdir") or "").strip()
59
- 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"]})
64
- return {"content": "已创建并排队:task_id=%s run_id=%s(可用 get_status 查进度)"
65
- % (task["id"], run["id"])}
66
-
67
-
68
- def _tool_get_status(args):
69
- from core import store
70
- task = store.get_task(str(args.get("task_id") or "").strip())
71
- if not task:
72
- return {"isError": True, "content": "任务不存在"}
73
- lines = ["任务:%s(%s)" % (task.get("title"), task.get("status"))]
74
- run = store.latest_run_by_task().get(task["id"])
75
- if run:
76
- lines.append("最近运行:%s(%s)" % (run.get("id"), run.get("status")))
77
- steps = [s for s in (run.get("steps") or []) if isinstance(s, dict)]
78
- if steps:
79
- cur = steps[-1]
80
- lines.append("步骤 %d/%d:%s(%s)" % (len(steps), run.get("total_steps") or len(steps),
81
- cur.get("title"), cur.get("status")))
82
- v = run.get("verdict") or {}
83
- if v.get("overall") is not None:
84
- lines.append("评分 %.1f(%s)" % (float(v["overall"]),
85
- "达标" if v.get("publishable") else "未达标"))
86
- if run.get("error"):
87
- lines.append("错误:%s" % str(run["error"])[:200])
88
- if task.get("git_state") == "isolated":
89
- lines.append("⚠ 任务分支待裁决(合并/丢弃)")
90
- return {"content": "\n".join(lines)}
91
-
92
-
93
- def _tool_list_recent(args):
94
- from core import store
95
- try:
96
- limit = max(1, min(30, int(args.get("limit") or 10)))
97
- except (TypeError, ValueError):
98
- limit = 10
99
- rows = [{"id": r.get("id"), "title": r.get("title"), "status": r.get("status"),
100
- "ts": r.get("started_at") or ""} for r in store.list_runs(limit=limit)]
101
- return {"content": json.dumps(rows, ensure_ascii=False, indent=1)}
102
-
103
-
104
- def _tool_usage_summary(args):
105
- from core import usage
106
- today, month = usage.cost_snapshot()
107
- tokens = sum(int(r.get("total") or 0) for r in usage._iter_records(31))
108
- return {"content": "今日花费 ¥%.2f\n本月花费 ¥%.2f\n近 31 天 tokens:%d(未标定单价的调用不计钱)"
109
- % (today, month, tokens)}
110
-
111
-
112
- def _tool_bench(args):
113
- from core import evalbench
114
- try:
115
- top = max(1, min(10, int(args.get("top") or 5)))
116
- except (TypeError, ValueError):
117
- top = 5
118
- board = evalbench.state().get("leaderboard") or []
119
- rows = [{"rank": r["rank"], "model": r["model"], "overall": r["overall"],
120
- "provider": r.get("provider_name")} for r in board[:top]]
121
- return {"content": json.dumps(rows, ensure_ascii=False, indent=1)}
122
-
123
-
124
- _TOOL_IMPL = {"create_task": _tool_create_task, "get_status": _tool_get_status,
125
- "list_recent": _tool_list_recent, "usage_summary": _tool_usage_summary,
126
- "bench_leaderboard": _tool_bench}
127
-
128
-
129
- def handle_request(msg):
130
- """处理一条客户端请求,返回响应 dict(通知返回 None)。"""
131
- if not isinstance(msg, dict) or msg.get("method") is None:
132
- return None
133
- rid = msg.get("id")
134
- method = str(msg["method"])
135
- if method == "initialize":
136
- return {"jsonrpc": "2.0", "id": rid, "result": {
137
- "protocolVersion": PROTOCOL_VERSION, "capabilities": {"tools": {}},
138
- "serverInfo": SERVER_INFO}}
139
- if method.startswith("notifications/"):
140
- return None
141
- if method == "tools/list":
142
- return {"jsonrpc": "2.0", "id": rid, "result": {"tools": TOOLS}}
143
- if method == "tools/call":
144
- params = msg.get("params") or {}
145
- name = str(params.get("name") or "")
146
- fn = _TOOL_IMPL.get(name)
147
- if fn is None:
148
- return {"jsonrpc": "2.0", "id": rid, "result": {
149
- "isError": True, "content": "未知工具: %s" % name}}
150
- try:
151
- out = fn(params.get("arguments") or {})
152
- except Exception as e:
153
- out = {"isError": True, "content": "工具执行失败: %s" % e}
154
- return {"jsonrpc": "2.0", "id": rid, "result": out}
155
- return {"jsonrpc": "2.0", "id": rid,
156
- "error": {"code": -32601, "message": "no such method: %s" % method}}
157
-
158
-
159
- def main():
160
- for raw in sys.stdin:
161
- line = raw.strip()
162
- if not line.startswith("{"):
163
- continue
164
- try:
165
- msg = json.loads(line)
166
- except Exception:
167
- continue
168
- resp = handle_request(msg)
169
- if resp is not None:
170
- sys.stdout.write(json.dumps(resp, ensure_ascii=False) + "\n")
171
- sys.stdout.flush()
172
-
173
-
174
- if __name__ == "__main__":
175
- main()
1
+ # -*- coding: utf-8 -*-
2
+ """CodeBee 的 MCP 服务器形态:其他 AI 客户端(Claude Code / ZCode / Cursor)
3
+ 把这里当工具服务器挂载,即可直接创建任务、查状态、看用量与评测榜单——
4
+ CodeBee 从编排工具变成可以被别的智能体调度的生态节点。
5
+
6
+ stdio JSON-RPC 2.0(按行分帧,与 core/mcp_client 同一传输约定)。启动:
7
+ python -X utf8 <CodeBee目录>/app/mcp_server.py
8
+ 客户端配置示例(Claude Code / 兼容客户端):
9
+ {"mcpServers": {"codebee": {"command": "python",
10
+ "args": ["-X", "utf8", "<CodeBee目录>/app/mcp_server.py"]}}}
11
+ 仅本机 stdio,无网络监听,凭据不经过此通道;create_task 会真实调度智能体
12
+ 执行(与界面上建任务同一条链)。
13
+ """
14
+ from __future__ import annotations
15
+
16
+ import json
17
+ import sys
18
+
19
+ PROTOCOL_VERSION = "2024-11-05"
20
+ SERVER_INFO = {"name": "codebee", "version": "1.0"}
21
+
22
+ TOOLS = [
23
+ {"name": "create_task", "description": "创建并启动一个 CodeBee 编排任务(真实调度智能体执行,与界面建任务同链)。返回 task_id 与 run_id。",
24
+ "inputSchema": {"type": "object", "properties": {
25
+ "goal": {"type": "string", "description": "任务目标(一句话说清要做什么)"},
26
+ "type": {"type": "string", "description": "任务类型 id(如 code/novel/doc/direct),默认 doc"},
27
+ "workdir": {"type": "string", "description": "工作目录绝对路径;留空用 CodeBee 默认保存路径"},
28
+ "title": {"type": "string", "description": "任务标题;留空取目标前 30 字"}},
29
+ "required": ["goal"]}},
30
+ {"name": "get_status", "description": "查任务与最近一次运行的状态:进度、当前步骤、评分、错误。",
31
+ "inputSchema": {"type": "object", "properties": {
32
+ "task_id": {"type": "string", "description": "任务 ID"}}, "required": ["task_id"]}},
33
+ {"name": "list_recent", "description": "列出最近的运行(id/标题/状态/时间)。",
34
+ "inputSchema": {"type": "object", "properties": {
35
+ "limit": {"type": "integer", "description": "条数,默认 10,最大 30"}}}},
36
+ {"name": "usage_summary", "description": "用量台账汇总:今日/本月花费(¥)与 token 总量。",
37
+ "inputSchema": {"type": "object", "properties": {}}},
38
+ {"name": "bench_leaderboard", "description": "模型评测基准台能力榜(综合分 Top N)。",
39
+ "inputSchema": {"type": "object", "properties": {
40
+ "top": {"type": "integer", "description": "取前 N,默认 5"}}}},
41
+ ]
42
+
43
+
44
+ def _enqueue(job):
45
+ """thin wrapper:便于单测 mock(真链会拉起 worker 执行)。"""
46
+ from core import jobs
47
+ jobs.enqueue(job)
48
+
49
+
50
+ def _tool_create_task(args):
51
+ from core import settings as settings_mod, store
52
+ goal = str(args.get("goal") or "").strip()
53
+ if not goal:
54
+ return {"isError": True, "content": "goal 不能为空"}
55
+ payload = {"type": str(args.get("type") or "doc").strip()[:40] or "doc",
56
+ "title": (str(args.get("title") or "").strip() or goal[:30])[:40],
57
+ "goal": goal[:4000],
58
+ "workdir": str(args.get("workdir") or "").strip()
59
+ 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"]})
64
+ return {"content": "已创建并排队:task_id=%s run_id=%s(可用 get_status 查进度)"
65
+ % (task["id"], run["id"])}
66
+
67
+
68
+ def _tool_get_status(args):
69
+ from core import store
70
+ task = store.get_task(str(args.get("task_id") or "").strip())
71
+ if not task:
72
+ return {"isError": True, "content": "任务不存在"}
73
+ lines = ["任务:%s(%s)" % (task.get("title"), task.get("status"))]
74
+ run = store.latest_run_by_task().get(task["id"])
75
+ if run:
76
+ lines.append("最近运行:%s(%s)" % (run.get("id"), run.get("status")))
77
+ steps = [s for s in (run.get("steps") or []) if isinstance(s, dict)]
78
+ if steps:
79
+ cur = steps[-1]
80
+ lines.append("步骤 %d/%d:%s(%s)" % (len(steps), run.get("total_steps") or len(steps),
81
+ cur.get("title"), cur.get("status")))
82
+ v = run.get("verdict") or {}
83
+ if v.get("overall") is not None:
84
+ lines.append("评分 %.1f(%s)" % (float(v["overall"]),
85
+ "达标" if v.get("publishable") else "未达标"))
86
+ if run.get("error"):
87
+ lines.append("错误:%s" % str(run["error"])[:200])
88
+ if task.get("git_state") == "isolated":
89
+ lines.append("⚠ 任务分支待裁决(合并/丢弃)")
90
+ return {"content": "\n".join(lines)}
91
+
92
+
93
+ def _tool_list_recent(args):
94
+ from core import store
95
+ try:
96
+ limit = max(1, min(30, int(args.get("limit") or 10)))
97
+ except (TypeError, ValueError):
98
+ limit = 10
99
+ rows = [{"id": r.get("id"), "title": r.get("title"), "status": r.get("status"),
100
+ "ts": r.get("started_at") or ""} for r in store.list_runs(limit=limit)]
101
+ return {"content": json.dumps(rows, ensure_ascii=False, indent=1)}
102
+
103
+
104
+ def _tool_usage_summary(args):
105
+ from core import usage
106
+ today, month = usage.cost_snapshot()
107
+ tokens = sum(int(r.get("total") or 0) for r in usage._iter_records(31))
108
+ return {"content": "今日花费 ¥%.2f\n本月花费 ¥%.2f\n近 31 天 tokens:%d(未标定单价的调用不计钱)"
109
+ % (today, month, tokens)}
110
+
111
+
112
+ def _tool_bench(args):
113
+ from core import evalbench
114
+ try:
115
+ top = max(1, min(10, int(args.get("top") or 5)))
116
+ except (TypeError, ValueError):
117
+ top = 5
118
+ board = evalbench.state().get("leaderboard") or []
119
+ rows = [{"rank": r["rank"], "model": r["model"], "overall": r["overall"],
120
+ "provider": r.get("provider_name")} for r in board[:top]]
121
+ return {"content": json.dumps(rows, ensure_ascii=False, indent=1)}
122
+
123
+
124
+ _TOOL_IMPL = {"create_task": _tool_create_task, "get_status": _tool_get_status,
125
+ "list_recent": _tool_list_recent, "usage_summary": _tool_usage_summary,
126
+ "bench_leaderboard": _tool_bench}
127
+
128
+
129
+ def handle_request(msg):
130
+ """处理一条客户端请求,返回响应 dict(通知返回 None)。"""
131
+ if not isinstance(msg, dict) or msg.get("method") is None:
132
+ return None
133
+ rid = msg.get("id")
134
+ method = str(msg["method"])
135
+ if method == "initialize":
136
+ return {"jsonrpc": "2.0", "id": rid, "result": {
137
+ "protocolVersion": PROTOCOL_VERSION, "capabilities": {"tools": {}},
138
+ "serverInfo": SERVER_INFO}}
139
+ if method.startswith("notifications/"):
140
+ return None
141
+ if method == "tools/list":
142
+ return {"jsonrpc": "2.0", "id": rid, "result": {"tools": TOOLS}}
143
+ if method == "tools/call":
144
+ params = msg.get("params") or {}
145
+ name = str(params.get("name") or "")
146
+ fn = _TOOL_IMPL.get(name)
147
+ if fn is None:
148
+ return {"jsonrpc": "2.0", "id": rid, "result": {
149
+ "isError": True, "content": "未知工具: %s" % name}}
150
+ try:
151
+ out = fn(params.get("arguments") or {})
152
+ except Exception as e:
153
+ out = {"isError": True, "content": "工具执行失败: %s" % e}
154
+ return {"jsonrpc": "2.0", "id": rid, "result": out}
155
+ return {"jsonrpc": "2.0", "id": rid,
156
+ "error": {"code": -32601, "message": "no such method: %s" % method}}
157
+
158
+
159
+ def main():
160
+ for raw in sys.stdin:
161
+ line = raw.strip()
162
+ if not line.startswith("{"):
163
+ continue
164
+ try:
165
+ msg = json.loads(line)
166
+ except Exception:
167
+ continue
168
+ resp = handle_request(msg)
169
+ if resp is not None:
170
+ sys.stdout.write(json.dumps(resp, ensure_ascii=False) + "\n")
171
+ sys.stdout.flush()
172
+
173
+
174
+ if __name__ == "__main__":
175
+ main()