codebee 0.1.71 → 0.1.73

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 (118) hide show
  1. package/CHANGELOG.md +601 -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 +8 -8
  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 +25 -1
  38. package/app/core/manager.py +2208 -2143
  39. package/app/core/market.py +90 -4
  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 +145 -138
  47. package/app/core/pipeline.py +102 -30
  48. package/app/core/planner.py +2 -0
  49. package/app/core/plugins.py +34 -0
  50. package/app/core/portguard.py +111 -111
  51. package/app/core/preview.py +293 -293
  52. package/app/core/prompt_guard.py +78 -0
  53. package/app/core/publish/__init__.py +7 -7
  54. package/app/core/publish/auto.py +396 -367
  55. package/app/core/publish/browser.py +655 -655
  56. package/app/core/publish/fanqie.py +165 -165
  57. package/app/core/publish/flow.py +640 -589
  58. package/app/core/publish/flows-fanqie-calibrated.json +336 -336
  59. package/app/core/publish/ledger.py +307 -229
  60. package/app/core/publish/manager.py +1118 -824
  61. package/app/core/publish/qimao.py +176 -146
  62. package/app/core/publish/ws.py +139 -139
  63. package/app/core/quality_gate.py +194 -0
  64. package/app/core/redact.py +38 -38
  65. package/app/core/repeat_guard.py +129 -129
  66. package/app/core/router.py +398 -398
  67. package/app/core/runner.py +2142 -2142
  68. package/app/core/selfupdate.py +384 -384
  69. package/app/core/session_log.py +161 -161
  70. package/app/core/sessions.py +312 -312
  71. package/app/core/settings_schema.py +270 -270
  72. package/app/core/share_page.py +114 -114
  73. package/app/core/skill_scan.py +139 -122
  74. package/app/core/skillpacks/fanqie-novel.md +98 -80
  75. package/app/core/skillpacks/market/character-bible.md +66 -66
  76. package/app/core/skillpacks/market/code-risk-checklist.md +58 -58
  77. package/app/core/skillpacks/market/git-workflow.md +57 -57
  78. package/app/core/skillpacks/market/release-notes.md +72 -72
  79. package/app/core/skillpacks/market/weekly-report.md +71 -71
  80. package/app/core/skillpacks/market/worldview-consistency.md +70 -70
  81. package/app/core/skillpacks/qimao-signing.md +70 -59
  82. package/app/core/skills.py +58 -8
  83. package/app/core/step_runner.py +132 -132
  84. package/app/core/store.py +51 -0
  85. package/app/core/task_compile.py +199 -199
  86. package/app/core/telemetry.py +292 -292
  87. package/app/core/tlsctx.py +64 -64
  88. package/app/core/token_meter.py +147 -147
  89. package/app/core/tracing.py +144 -144
  90. package/app/core/upstream.py +41 -41
  91. package/app/core/usage.py +1035 -1035
  92. package/app/core/volumes.py +389 -389
  93. package/app/core/wxdigest.py +858 -858
  94. package/app/core/zentao.py +2692 -2614
  95. package/app/main.py +110 -39
  96. package/app/mcp_server.py +175 -175
  97. package/app/pick_dialog.py +110 -110
  98. package/app/ui/app.js +509 -114
  99. package/app/ui/artdiff.js +74 -74
  100. package/app/ui/board.html +162 -162
  101. package/app/ui/board.js +305 -305
  102. package/app/ui/i18n.js +41 -2
  103. package/app/ui/icons/bee.svg +79 -79
  104. package/app/ui/index.html +8 -3
  105. package/app/ui/manifest.json +16 -16
  106. package/app/ui/qrcode.js +2297 -2297
  107. package/app/ui/style.css +74 -26
  108. package/bin/tutti.js +147 -147
  109. package/package.json +48 -47
  110. package/tools/wx_reader.py +261 -261
  111. package/app/.playwright-cli/console-2026-09-23T08-26-03-635Z.log +0 -88
  112. package/app/.playwright-cli/page-2026-09-23T08-26-03-845Z.yml +0 -126
  113. package/app/.playwright-cli/page-2026-09-23T08-29-44-203Z.png +0 -0
  114. package/app/.playwright-cli/page-2026-09-23T08-35-42-046Z.yml +0 -210
  115. package/app/.playwright-cli/page-2026-09-23T08-36-52-858Z.png +0 -0
  116. package/app/.playwright-cli/page-2026-09-23T08-37-33-938Z.png +0 -0
  117. package/app/.playwright-cli/run-detail-after.png +0 -0
  118. 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,27 @@ 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)
306
+ if path == "/api/plugins":
307
+ # 本地插件(.codex-plugin/plugin.json 契约):发现+安装台账的
308
+ # 聚合视图。坏 manifest 以 blocked/broken 行呈现,不 500。
309
+ from core import plugins
310
+ return self._json(200, plugins.view())
291
311
  if path == "/api/knowledge":
292
312
  from core import knowledge
293
313
  return self._json(200, knowledge.view())
@@ -309,6 +329,14 @@ class Handler(BaseHTTPRequestHandler):
309
329
  if path == "/api/evalbench":
310
330
  from core import evalbench
311
331
  return self._json(200, evalbench.state())
332
+ if path == "/api/evalbench/report":
333
+ # 评测报告导出(Markdown,浏览器下载)
334
+ from core import evalbench
335
+ return self._send(200, evalbench.report_markdown().encode("utf-8"),
336
+ ctype="text/markdown; charset=utf-8",
337
+ headers={"Content-Disposition":
338
+ 'attachment; filename="codebee-bench-%s.md"'
339
+ % time.strftime("%Y%m%d-%H%M%S")})
312
340
  if path == "/api/catalog":
313
341
  return self._json(200, {"catalog": manager.catalog_view(),
314
342
  "checking": manager.updates_checking()})
@@ -533,11 +561,15 @@ class Handler(BaseHTTPRequestHandler):
533
561
  return self._json(200, {"path": fp, "text": text})
534
562
  m = re.match(r"^/api/tasks/([^/]+)/book-meta$", path)
535
563
  if m:
536
- # 作品信息(番茄/七猫建书表单资料):读取任务上的生成状态与结果
564
+ # 作品信息(番茄/七猫建书表单资料):读取生成状态与结果。
565
+ # 连载链沿链继承(都是用第一个):续写批次读到的就是首批的结果。
537
566
  task = store.get_task(m.group(1))
538
567
  if not task:
539
568
  return self._json(404, {"error": "not found"})
540
- return self._json(200, {"book_meta": task.get("book_meta") or {}})
569
+ return self._json(200, {"book_meta": {
570
+ p: store.inherited_book_meta(task, p)
571
+ for p in ("fanqie", "qimao")
572
+ if store.inherited_book_meta(task, p)}})
541
573
  if path == "/api/publish":
542
574
  # 一键发布:平台连接状态 + 最近台账(详情页发布面板)
543
575
  from core.publish import manager as pub
@@ -549,7 +581,8 @@ class Handler(BaseHTTPRequestHandler):
549
581
  from core.publish import manager as pub
550
582
  from core.publish import ledger as pub_ledger
551
583
  tid = m.group(1)
552
- books = pub_ledger.load_books().get(tid) or {}
584
+ # 绑定沿连载链继承(都是用第一个):续写批次也要看到这本书的登记
585
+ books = pub_ledger.books_for(tid)
553
586
  return self._json(200, {"history": pub.history(task_id=tid, limit=50),
554
587
  "books": books,
555
588
  "published": {p: pub.published_local(tid, p)
@@ -937,6 +970,15 @@ class Handler(BaseHTTPRequestHandler):
937
970
  m = re.match(r"^/api/publish/task/([^/]+)/(create-book|chapter|auto-publish)$", path)
938
971
  if m:
939
972
  return self._api_publish_task_op(m.group(1), m.group(2))
973
+ m = re.match(r"^/api/publish/task/([^/]+)/confirm-chapter$", path)
974
+ if m:
975
+ from core.publish import manager as pub
976
+ body = self._body() or {}
977
+ platform = (body.get("platform") or "").strip()
978
+ ok, err = pub.confirm_manual_chapter(
979
+ m.group(1), platform, body.get("chapter_no"))
980
+ return self._json(400, {"error": err or "确认失败"}) \
981
+ if not ok else self._json(200, {"ok": True})
940
982
  m = re.match(r"^/api/publish/task/([^/]+)/register-book$", path)
941
983
  if m:
942
984
  return self._api_publish_register_book(m.group(1))
@@ -960,7 +1002,9 @@ class Handler(BaseHTTPRequestHandler):
960
1002
  return self._json(404, {"error": "任务不存在"})
961
1003
  body = self._body() or {}
962
1004
  plat = str(body.get("platform") or "").strip()
963
- books = pub_ledger.load_books().get(tid) or {}
1005
+ # 绑定沿连载链继承(与 history 端点同口径):续写批次发起校准也能
1006
+ # 落到这本书的账上,否则前端看到继承绑定、这里却报「未登记」互相打架
1007
+ books = pub_ledger.books_for(tid)
964
1008
  if plat and plat not in books:
965
1009
  return self._json(400, {"error": "该平台未登记作品"})
966
1010
  plats = [plat] if plat else list(books)
@@ -1267,6 +1311,23 @@ class Handler(BaseHTTPRequestHandler):
1267
1311
  from core import knowledge
1268
1312
  n = knowledge.learn_from_run((self._body().get("run_id") or ""))
1269
1313
  return self._json(200, {"ok": True, "learned": n})
1314
+ m = re.match(r"^/api/plugins/([^/]+)/(install|enable|disable|remove)$", path)
1315
+ if m:
1316
+ # 本地插件生命周期:只搬文件+记账(skills 走 market.install_files、
1317
+ # MCP 只暴露声明),永不执行插件代码。前端 loadPlugins 拉的就是
1318
+ # 上面 GET /api/plugins;缺这组路由时设置页会显示「插件加载失败」。
1319
+ from core import plugins
1320
+ pid, op = m.group(1), m.group(2)
1321
+ if op == "install":
1322
+ result, err = plugins.install(pid)
1323
+ if err:
1324
+ return self._json(400, {"error": err})
1325
+ return self._json(200, result or {"ok": True})
1326
+ if op == "remove":
1327
+ err = plugins.remove(pid)
1328
+ return self._json(400, {"error": err}) if err else self._json(200, {"ok": True})
1329
+ err = plugins.toggle(pid, op == "enable")
1330
+ return self._json(400, {"error": err}) if err else self._json(200, {"ok": True})
1270
1331
  m = re.match(r"^/api/flows/([^/]+)/reset$", path)
1271
1332
  if m:
1272
1333
  err = flows.reset_flow(m.group(1))
@@ -1568,6 +1629,14 @@ class Handler(BaseHTTPRequestHandler):
1568
1629
  except zentao.ZenError as e:
1569
1630
  return self._json(404, {"error": str(e)})
1570
1631
  return self._json(200, {"ok": True, "claim": c})
1632
+ if path == "/api/zentao/claims/retry":
1633
+ from core import zentao
1634
+ body = self._body() or {}
1635
+ try:
1636
+ c, msg = zentao.retry_claim(body.get("bug_id"))
1637
+ except zentao.ZenError as e:
1638
+ return self._json(409, {"error": str(e)})
1639
+ return self._json(200, {"ok": True, "claim": c, "message": msg})
1571
1640
  if path == "/api/zentao/modules":
1572
1641
  from core import zentao
1573
1642
  pid = (self._body() or {}).get("product")
@@ -1714,7 +1783,13 @@ class Handler(BaseHTTPRequestHandler):
1714
1783
  if platform not in bookmeta.PLATFORMS:
1715
1784
  return self._json(400, {"error": "platform 必须是 fanqie 或 qimao"})
1716
1785
  if not bookmeta.needs_book_meta(task):
1717
- return self._json(400, {"error": "只有连载首批任务需要作品信息;续写批次沿用第一批的开书资料"})
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 "只有连载首批任务需要作品信息;续写批次沿用第一批的开书资料")})
1718
1793
  if task.get("status") in ("queued", "running"):
1719
1794
  return self._json(400, {"error": "任务正在运行,请等本轮结束后再生成作品信息"})
1720
1795
  cur = ((task.get("book_meta") or {}).get(platform) or {})
@@ -1759,8 +1834,14 @@ class Handler(BaseHTTPRequestHandler):
1759
1834
  if platform not in ("fanqie", "qimao"):
1760
1835
  return self._json(400, {"error": "platform 必须是 fanqie 或 qimao"})
1761
1836
  auto_submit = bool(body.get("auto_submit"))
1837
+ force = bool(body.get("force"))
1838
+ force_confirmed = bool(body.get("force_confirmed"))
1839
+ force_reason = str(body.get("force_reason") or "").strip()
1762
1840
  if op == "create-book":
1763
- ok, err = pub.create_book_async(task_id, platform, auto_submit)
1841
+ kwargs = ({"force": force, "force_confirmed": force_confirmed,
1842
+ "force_reason": force_reason}
1843
+ if force or force_confirmed or force_reason else {})
1844
+ ok, err = pub.create_book_async(task_id, platform, auto_submit, **kwargs)
1764
1845
  elif op == "auto-publish":
1765
1846
  # 定时发布配置(P2.5):enabled=false 也落(保留 time 供再开);
1766
1847
  # 校验/归一在 auto.norm_auto_publish,语义见 auto.py 头注
@@ -1789,9 +1870,16 @@ class Handler(BaseHTTPRequestHandler):
1789
1870
  fp.relative_to(_P(wd).resolve())
1790
1871
  except (OSError, ValueError):
1791
1872
  return self._json(400, {"error": "章节文件必须在任务工作目录内"})
1792
- ok, err = pub.upload_chapter_async(task_id, platform, str(fp), auto_submit)
1873
+ kwargs = ({"force": force, "force_confirmed": force_confirmed,
1874
+ "force_reason": force_reason}
1875
+ if force or force_confirmed or force_reason else {})
1876
+ ok, err = pub.upload_chapter_async(task_id, platform, str(fp), auto_submit,
1877
+ **kwargs)
1793
1878
  if not ok:
1794
- return self._json(400, {"error": err or "操作失败"})
1879
+ payload = {"error": err or "操作失败"}
1880
+ if str(err or "").startswith("质量门禁拦截:"):
1881
+ payload["quality_gate"] = {"status": "blocked", "message": err}
1882
+ return self._json(400, payload)
1795
1883
  return self._json(200, {"ok": True, "started": True})
1796
1884
 
1797
1885
  def _api_publish_register_book(self, task_id):
@@ -1821,10 +1909,19 @@ class Handler(BaseHTTPRequestHandler):
1821
1909
  platform = (body.get("platform") or "").strip()
1822
1910
  if platform not in ("fanqie", "qimao"):
1823
1911
  return self._json(400, {"error": "platform 必须是 fanqie 或 qimao"})
1912
+ force = bool(body.get("force"))
1913
+ force_confirmed = bool(body.get("force_confirmed"))
1914
+ force_reason = str(body.get("force_reason") or "").strip()
1824
1915
  ok, err = pub_auto.publish_pending_async(
1825
- task_id, platform, auto_submit=bool(body.get("auto_submit")))
1916
+ task_id, platform, auto_submit=bool(body.get("auto_submit")),
1917
+ **({"force": force, "force_confirmed": force_confirmed,
1918
+ "force_reason": force_reason}
1919
+ if force or force_confirmed or force_reason else {}))
1826
1920
  if not ok:
1827
- return self._json(400, {"error": err or "操作失败"})
1921
+ payload = {"error": err or "操作失败"}
1922
+ if str(err or "").startswith("质量门禁拦截:"):
1923
+ payload["quality_gate"] = {"status": "blocked", "message": err}
1924
+ return self._json(400, payload)
1828
1925
  return self._json(200, {"ok": True, "started": True})
1829
1926
 
1830
1927
  def _api_task_side(self, task_id):
@@ -3019,36 +3116,10 @@ def main():
3019
3116
  from core import notify as _notify
3020
3117
  usage_ledger.set_alert_push(_notify.push_text) # 花费预警推送注入(防 usage→notify 静态环)
3021
3118
  from core import mcp_client as _mcp
3119
+ from core.plugins import merged_servers_text as _plugins_mcp_text
3022
3120
  def _mcp_servers_text():
3023
3121
  """settings 清单 + 已启用插件的 MCP 服务器合成(plugin 名隔离防撞名)。"""
3024
- import json as _json
3025
- try:
3026
- raw = str(settings.load().get("mcp_servers") or "")
3027
- configured = _json.loads(raw) if raw.strip() else []
3028
- except Exception:
3029
- return raw or ""
3030
- if not isinstance(configured, list):
3031
- return raw or ""
3032
- try:
3033
- from core import plugins as _plugins
3034
- plugin_servers = _plugins.active_mcp_servers()
3035
- except Exception:
3036
- plugin_servers = [] # 坏插件不能连累用户显式配置的服务器
3037
- import hashlib as _hashlib
3038
- import re as _re
3039
- for item in plugin_servers or []:
3040
- if not isinstance(item, dict):
3041
- continue
3042
- plugin_id = str(item.pop("plugin_id", "plugin"))
3043
- server_name = str(item.get("name") or "server")
3044
- digest = _hashlib.sha256(
3045
- (plugin_id + ":" + server_name).encode("utf-8")).hexdigest()[:6]
3046
- safe = _re.sub(r"[^a-z0-9_-]", "-", plugin_id.lower())
3047
- item["name"] = ("plg_" + safe[:11] + "_" + digest)[:24]
3048
- if not any(x.get("name") == item["name"] for x in configured
3049
- if isinstance(x, dict)):
3050
- configured.append(item)
3051
- return _json.dumps(configured, ensure_ascii=False)
3122
+ return _plugins_mcp_text(settings.load().get("mcp_servers"))
3052
3123
  _mcp.set_settings_text(_mcp_servers_text) # MCP 清单注入(防 mcp_client→settings 边)
3053
3124
  _step("正在回填用量台账…")
3054
3125
  n_bf = usage.backfill_from_runs() # 历史运行 token 回填台账(幂等,仅补缺失步骤)
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()