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.
- package/CHANGELOG.md +595 -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 +7 -7
- 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 +21 -0
- package/app/core/manager.py +2143 -2143
- package/app/core/market.py +88 -2
- 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 +138 -138
- package/app/core/pipeline.py +102 -30
- 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 +589 -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 +931 -824
- package/app/core/publish/qimao.py +146 -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 +80 -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/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 +86 -11
- package/app/mcp_server.py +175 -175
- package/app/pick_dialog.py +110 -110
- package/app/ui/app.js +258 -54
- 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 +24 -0
- package/app/ui/icons/bee.svg +79 -79
- package/app/ui/index.html +5 -0
- package/app/ui/manifest.json +16 -16
- package/app/ui/qrcode.js +2297 -2297
- package/app/ui/style.css +7051 -7012
- 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,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
|
-
|
|
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":
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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()
|