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.
- package/CHANGELOG.md +601 -589
- package/LICENSE +21 -21
- package/README.md +508 -509
- package/app/core/acceptance.py +100 -100
- package/app/core/aiflavor.py +197 -197
- package/app/core/artifacts.py +148 -148
- package/app/core/assets.py +110 -110
- package/app/core/attachments.py +623 -623
- package/app/core/audit.py +30 -30
- package/app/core/backup.py +470 -470
- package/app/core/beekeeper.py +183 -183
- package/app/core/benchstore.py +95 -95
- package/app/core/board.py +209 -209
- package/app/core/bookmeta.py +935 -935
- package/app/core/bookmeta_catalog.py +180 -180
- package/app/core/branching.py +137 -137
- package/app/core/builtin_agent.py +1680 -1673
- package/app/core/capability.py +144 -144
- package/app/core/chaptersafe.py +32 -32
- package/app/core/cleanup.py +303 -303
- package/app/core/compaction.py +201 -201
- package/app/core/covergen.py +356 -356
- package/app/core/defectretro.py +43 -43
- package/app/core/diagnostics.py +114 -114
- package/app/core/dispatch.py +190 -190
- package/app/core/dispatch_log.py +128 -128
- package/app/core/env_scrub.py +85 -85
- package/app/core/errorlog.py +137 -137
- package/app/core/evalbench.py +836 -720
- package/app/core/evaluation.py +174 -174
- package/app/core/failure_notify.py +290 -290
- package/app/core/flows.py +8 -8
- package/app/core/goal_service.py +167 -167
- package/app/core/health.py +374 -374
- package/app/core/history.py +38 -38
- package/app/core/hooks.py +162 -162
- package/app/core/jobs.py +25 -1
- package/app/core/manager.py +2208 -2143
- package/app/core/market.py +90 -4
- package/app/core/market_remote.py +16 -0
- package/app/core/mocks.py +64 -64
- package/app/core/modelhub.py +3 -2
- package/app/core/notify.py +201 -201
- package/app/core/novel_quality.py +189 -189
- package/app/core/operations.py +228 -228
- package/app/core/paihang.py +145 -138
- package/app/core/pipeline.py +102 -30
- package/app/core/planner.py +2 -0
- package/app/core/plugins.py +34 -0
- package/app/core/portguard.py +111 -111
- package/app/core/preview.py +293 -293
- package/app/core/prompt_guard.py +78 -0
- package/app/core/publish/__init__.py +7 -7
- package/app/core/publish/auto.py +396 -367
- package/app/core/publish/browser.py +655 -655
- package/app/core/publish/fanqie.py +165 -165
- package/app/core/publish/flow.py +640 -589
- package/app/core/publish/flows-fanqie-calibrated.json +336 -336
- package/app/core/publish/ledger.py +307 -229
- package/app/core/publish/manager.py +1118 -824
- package/app/core/publish/qimao.py +176 -146
- package/app/core/publish/ws.py +139 -139
- package/app/core/quality_gate.py +194 -0
- package/app/core/redact.py +38 -38
- package/app/core/repeat_guard.py +129 -129
- package/app/core/router.py +398 -398
- package/app/core/runner.py +2142 -2142
- package/app/core/selfupdate.py +384 -384
- package/app/core/session_log.py +161 -161
- package/app/core/sessions.py +312 -312
- package/app/core/settings_schema.py +270 -270
- package/app/core/share_page.py +114 -114
- package/app/core/skill_scan.py +139 -122
- package/app/core/skillpacks/fanqie-novel.md +98 -80
- package/app/core/skillpacks/market/character-bible.md +66 -66
- package/app/core/skillpacks/market/code-risk-checklist.md +58 -58
- package/app/core/skillpacks/market/git-workflow.md +57 -57
- package/app/core/skillpacks/market/release-notes.md +72 -72
- package/app/core/skillpacks/market/weekly-report.md +71 -71
- package/app/core/skillpacks/market/worldview-consistency.md +70 -70
- package/app/core/skillpacks/qimao-signing.md +70 -59
- package/app/core/skills.py +58 -8
- package/app/core/step_runner.py +132 -132
- package/app/core/store.py +51 -0
- package/app/core/task_compile.py +199 -199
- package/app/core/telemetry.py +292 -292
- package/app/core/tlsctx.py +64 -64
- package/app/core/token_meter.py +147 -147
- package/app/core/tracing.py +144 -144
- package/app/core/upstream.py +41 -41
- package/app/core/usage.py +1035 -1035
- package/app/core/volumes.py +389 -389
- package/app/core/wxdigest.py +858 -858
- package/app/core/zentao.py +2692 -2614
- package/app/main.py +110 -39
- package/app/mcp_server.py +175 -175
- package/app/pick_dialog.py +110 -110
- package/app/ui/app.js +509 -114
- package/app/ui/artdiff.js +74 -74
- package/app/ui/board.html +162 -162
- package/app/ui/board.js +305 -305
- package/app/ui/i18n.js +41 -2
- package/app/ui/icons/bee.svg +79 -79
- package/app/ui/index.html +8 -3
- package/app/ui/manifest.json +16 -16
- package/app/ui/qrcode.js +2297 -2297
- package/app/ui/style.css +74 -26
- package/bin/tutti.js +147 -147
- package/package.json +48 -47
- package/tools/wx_reader.py +261 -261
- package/app/.playwright-cli/console-2026-09-23T08-26-03-635Z.log +0 -88
- package/app/.playwright-cli/page-2026-09-23T08-26-03-845Z.yml +0 -126
- package/app/.playwright-cli/page-2026-09-23T08-29-44-203Z.png +0 -0
- package/app/.playwright-cli/page-2026-09-23T08-35-42-046Z.yml +0 -210
- package/app/.playwright-cli/page-2026-09-23T08-36-52-858Z.png +0 -0
- package/app/.playwright-cli/page-2026-09-23T08-37-33-938Z.png +0 -0
- package/app/.playwright-cli/run-detail-after.png +0 -0
- 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
|
-
|
|
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":
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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()
|