codebee 0.1.103 → 0.1.105

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 (128) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/LICENSE +21 -21
  3. package/README.md +5 -3
  4. package/app/core/acceptance.py +100 -100
  5. package/app/core/acp_client.py +457 -457
  6. package/app/core/aiflavor.py +197 -197
  7. package/app/core/analytics.py +38 -38
  8. package/app/core/artifacts.py +148 -148
  9. package/app/core/assets.py +110 -110
  10. package/app/core/attachments.py +623 -623
  11. package/app/core/audit.py +30 -30
  12. package/app/core/backend_profiles.py +188 -188
  13. package/app/core/backup.py +470 -470
  14. package/app/core/beekeeper.py +183 -183
  15. package/app/core/benchstore.py +102 -102
  16. package/app/core/bookmeta.py +1036 -1036
  17. package/app/core/bookmeta_catalog.py +180 -180
  18. package/app/core/branching.py +137 -137
  19. package/app/core/capability.py +144 -144
  20. package/app/core/chaptersafe.py +32 -32
  21. package/app/core/checkpoints.py +48 -5
  22. package/app/core/cleanup.py +303 -303
  23. package/app/core/command_auth.py +302 -302
  24. package/app/core/compaction.py +201 -201
  25. package/app/core/contracts.py +255 -255
  26. package/app/core/covergen.py +356 -356
  27. package/app/core/defectretro.py +43 -43
  28. package/app/core/diagnostics.py +114 -114
  29. package/app/core/dispatch_log.py +128 -128
  30. package/app/core/env_scrub.py +85 -85
  31. package/app/core/errorlog.py +137 -137
  32. package/app/core/eval_matrix.py +91 -91
  33. package/app/core/evaluation.py +174 -174
  34. package/app/core/failure_notify.py +290 -290
  35. package/app/core/flow_graph.py +33 -33
  36. package/app/core/goal_service.py +167 -167
  37. package/app/core/health.py +389 -389
  38. package/app/core/history.py +38 -38
  39. package/app/core/hooks.py +162 -162
  40. package/app/core/knowledge_pipeline.py +86 -86
  41. package/app/core/manager.py +2386 -2385
  42. package/app/core/mocks.py +64 -64
  43. package/app/core/notify.py +201 -201
  44. package/app/core/novel_quality.py +285 -181
  45. package/app/core/openapi.py +246 -217
  46. package/app/core/operations.py +228 -228
  47. package/app/core/paihang.py +145 -145
  48. package/app/core/pipeline.py +81 -11
  49. package/app/core/planner.py +7 -0
  50. package/app/core/portguard.py +111 -111
  51. package/app/core/presence.py +77 -77
  52. package/app/core/preview.py +293 -293
  53. package/app/core/project_memory.py +252 -206
  54. package/app/core/prompt_guard.py +78 -78
  55. package/app/core/publish/__init__.py +7 -7
  56. package/app/core/publish/auto.py +659 -659
  57. package/app/core/publish/chaprows.py +138 -138
  58. package/app/core/publish/flows-fanqie-calibrated.json +442 -442
  59. package/app/core/publish/manager.py +1185 -1185
  60. package/app/core/publish/qimao.py +193 -193
  61. package/app/core/publish/volumes.py +108 -108
  62. package/app/core/publish/ws.py +139 -139
  63. package/app/core/quality_gate.py +201 -201
  64. package/app/core/redact.py +38 -38
  65. package/app/core/registry.py +35 -0
  66. package/app/core/repeat_guard.py +129 -129
  67. package/app/core/retrieval.py +137 -137
  68. package/app/core/review_requests.py +119 -0
  69. package/app/core/runner.py +5 -1
  70. package/app/core/sandbox_runtime.py +95 -95
  71. package/app/core/selfupdate.py +384 -384
  72. package/app/core/session_log.py +161 -161
  73. package/app/core/sessions.py +312 -312
  74. package/app/core/settings.py +215 -215
  75. package/app/core/settings_schema.py +282 -282
  76. package/app/core/share_page.py +114 -114
  77. package/app/core/skill_scan.py +139 -139
  78. package/app/core/skillpacks/market/character-bible.md +66 -66
  79. package/app/core/skillpacks/market/code-risk-checklist.md +58 -58
  80. package/app/core/skillpacks/market/git-workflow.md +57 -57
  81. package/app/core/skillpacks/market/release-notes.md +72 -72
  82. package/app/core/skillpacks/market/weekly-report.md +71 -71
  83. package/app/core/skillpacks/market/worldview-consistency.md +70 -70
  84. package/app/core/step_runner.py +132 -132
  85. package/app/core/story_tracking.py +275 -275
  86. package/app/core/task_compile.py +199 -199
  87. package/app/core/telemetry.py +292 -292
  88. package/app/core/tlsctx.py +64 -64
  89. package/app/core/token_meter.py +147 -147
  90. package/app/core/tool_outputs.py +107 -107
  91. package/app/core/tracing.py +207 -207
  92. package/app/core/upstream.py +41 -41
  93. package/app/core/usage.py +1038 -1038
  94. package/app/core/volumes.py +389 -389
  95. package/app/core/webhooks.py +89 -89
  96. package/app/core/wxdigest.py +865 -865
  97. package/app/core/zentao.py +2692 -2692
  98. package/app/main.py +43 -0
  99. package/app/mcp_server.py +244 -244
  100. package/app/pick_dialog.py +110 -110
  101. package/app/ui/app.js +21 -2
  102. package/app/ui/artdiff.js +74 -74
  103. package/app/ui/hive3d.js +54 -31
  104. package/app/ui/i18n.js +32 -1
  105. package/app/ui/icons/bee.svg +79 -79
  106. package/app/ui/index.html +1 -0
  107. package/app/ui/manifest.json +16 -16
  108. package/app/ui/qrcode.js +2297 -2297
  109. package/bin/tutti.js +147 -147
  110. package/package.json +1 -1
  111. package/tools/wx_reader.py +261 -261
  112. package/app/.playwright-cli/console-2026-09-23T08-26-03-635Z.log +0 -88
  113. package/app/.playwright-cli/page-2026-09-23T08-26-03-845Z.yml +0 -126
  114. package/app/.playwright-cli/page-2026-09-23T08-29-44-203Z.png +0 -0
  115. package/app/.playwright-cli/page-2026-09-23T08-35-42-046Z.yml +0 -210
  116. package/app/.playwright-cli/page-2026-09-23T08-36-52-858Z.png +0 -0
  117. package/app/.playwright-cli/page-2026-09-23T08-37-33-938Z.png +0 -0
  118. package/app/.playwright-cli/run-detail-after.png +0 -0
  119. package/app/output/_createbook_body.json +0 -1
  120. package/app/output/_probe_fq_click.py +0 -50
  121. package/app/output/_probe_fq_form.py +0 -42
  122. package/app/output/_probe_fq_modal.py +0 -55
  123. package/app/output/_probe_fq_points.py +0 -71
  124. package/app/output/_probe_fq_submit.py +0 -34
  125. package/app/output/_probe_fq_watch.py +0 -62
  126. package/app/output/workbench-clean-preview.jpg +0 -0
  127. package/app/output/workbench-reference-render.png +0 -0
  128. package/app/output/workbench-shell.png +0 -0
package/app/main.py CHANGED
@@ -358,6 +358,22 @@ class Handler(BaseHTTPRequestHandler):
358
358
  return self._json(200, {"entries": project_memory.list_entries(task.get("workdir"))})
359
359
  except (OSError, ValueError) as exc:
360
360
  return self._json(409, {"error": str(exc)})
361
+ m = re.match(r"^/api/tasks/([^/]+)/memory/preview$", path)
362
+ if m:
363
+ task = store.get_task(m.group(1))
364
+ if not task:
365
+ return self._json(404, {"error": "not found"})
366
+ from core import project_memory
367
+ qs = parse_qs(urlparse(self.path).query)
368
+ try:
369
+ return self._json(200, project_memory.injection_preview(
370
+ task.get("workdir"), query=(qs.get("q") or [""])[0],
371
+ cap=(qs.get("cap") or [4000])[0]))
372
+ except (OSError, ValueError) as exc:
373
+ return self._json(409, {"error": str(exc)})
374
+ if path == "/api/reviews":
375
+ from core import review_requests
376
+ return self._json(200, {"reviews": review_requests.list_requests()})
361
377
  if path == "/api/backend-profiles":
362
378
  from core import backend_profiles
363
379
  try:
@@ -419,6 +435,10 @@ class Handler(BaseHTTPRequestHandler):
419
435
  if path == "/api/catalog":
420
436
  return self._json(200, {"catalog": manager.catalog_view(),
421
437
  "checking": manager.updates_checking()})
438
+ if path == "/api/capabilities":
439
+ from core import registry
440
+ return self._json(200, {"capabilities": [
441
+ registry.capability_manifest(e) for e in catalog.load()]})
422
442
  if path == "/api/sessions":
423
443
  from core import sessions
424
444
  return self._json(200, {"sessions": sessions.scan(
@@ -968,6 +988,29 @@ class Handler(BaseHTTPRequestHandler):
968
988
  return self._json(200, {"ok": ok, "output": out})
969
989
  if path == "/api/tasks/clarify":
970
990
  return self._api_task_clarify()
991
+ if path == "/api/reviews":
992
+ from core import review_requests
993
+ body = self._body() or {}
994
+ try:
995
+ row = review_requests.create(
996
+ body.get("run_id"), summary=body.get("summary"),
997
+ paths=body.get("paths"), channel=body.get("channel"))
998
+ return self._json(200, {"review": row})
999
+ except (OSError, TypeError, ValueError) as exc:
1000
+ return self._json(400, {"error": str(exc)})
1001
+ m = re.match(r"^/api/reviews/([^/]+)/transition$", path)
1002
+ if m:
1003
+ from core import review_requests
1004
+ body = self._body() or {}
1005
+ try:
1006
+ row = review_requests.transition(
1007
+ unquote(m.group(1)), body.get("action"),
1008
+ reviewer=body.get("reviewer"), reason=body.get("reason"))
1009
+ return self._json(200, {"review": row})
1010
+ except KeyError as exc:
1011
+ return self._json(404, {"error": str(exc)})
1012
+ except (OSError, TypeError, ValueError) as exc:
1013
+ return self._json(400, {"error": str(exc)})
971
1014
  m = re.match(r"^/api/tasks/([^/]+)/memory$", path)
972
1015
  if m:
973
1016
  task = store.get_task(m.group(1))
package/app/mcp_server.py CHANGED
@@ -1,244 +1,244 @@
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
- "acceptance_criteria": {"type": "array", "items": {"type": "string"}},
30
- "approval_required": {"type": "boolean"}},
31
- "required": ["goal"]}},
32
- {"name": "get_status", "description": "查任务与最近一次运行的状态:进度、当前步骤、评分、错误。",
33
- "inputSchema": {"type": "object", "properties": {
34
- "task_id": {"type": "string", "description": "任务 ID"}}, "required": ["task_id"]}},
35
- {"name": "list_recent", "description": "列出最近的运行(id/标题/状态/时间)。",
36
- "inputSchema": {"type": "object", "properties": {
37
- "limit": {"type": "integer", "description": "条数,默认 10,最大 30"}}}},
38
- {"name": "usage_summary", "description": "用量台账汇总:今日/本月花费(¥)与 token 总量。",
39
- "inputSchema": {"type": "object", "properties": {}}},
40
- {"name": "bench_leaderboard", "description": "模型评测基准台能力榜(综合分 Top N)。",
41
- "inputSchema": {"type": "object", "properties": {
42
- "top": {"type": "integer", "description": "取前 N,默认 5"}}}},
43
- {"name": "get_contract", "description": "读取任务验收标准、证据、审批和完成回执。",
44
- "inputSchema": {"type": "object", "properties": {
45
- "task_id": {"type": "string"}}, "required": ["task_id"]}},
46
- {"name": "record_evidence", "description": "向任务契约追加结构化验收证据。",
47
- "inputSchema": {"type": "object", "properties": {
48
- "task_id": {"type": "string"}, "criterion": {"type": "string"},
49
- "status": {"type": "string", "enum": ["passed", "failed", "unknown", "not_evaluated"]},
50
- "summary": {"type": "string"}}, "required": ["task_id", "status"]}},
51
- {"name": "search_knowledge", "description": "在本地检索索引中搜索带来源和命中词解释的上下文。",
52
- "inputSchema": {"type": "object", "properties": {
53
- "query": {"type": "string"}, "limit": {"type": "integer"}}, "required": ["query"]}},
54
- ]
55
-
56
-
57
- def _enqueue(job):
58
- """thin wrapper:便于单测 mock(真链会拉起 worker 执行)。"""
59
- from core import jobs
60
- jobs.enqueue(job)
61
-
62
-
63
- def _tool_create_task(args):
64
- from core import settings as settings_mod, store
65
- goal = str(args.get("goal") or "").strip()
66
- if not goal:
67
- return {"isError": True, "content": "goal 不能为空"}
68
- payload = {"type": str(args.get("type") or "doc").strip()[:40] or "doc",
69
- "title": (str(args.get("title") or "").strip() or goal[:30])[:40],
70
- "goal": goal[:4000],
71
- "workdir": str(args.get("workdir") or "").strip()
72
- or settings_mod.default_workdir()}
73
- criteria = args.get("acceptance_criteria")
74
- if criteria is not None and not isinstance(criteria, list):
75
- return {"isError": True, "content": "acceptance_criteria 必须是列表"}
76
- if isinstance(criteria, list):
77
- payload["acceptance_criteria"] = [str(x).strip()[:500] for x in criteria if str(x).strip()][:40]
78
- approval_required = args.get("approval_required", False)
79
- if not isinstance(approval_required, bool):
80
- return {"isError": True, "content": "approval_required 必须是布尔值"}
81
- payload["approval_required"] = approval_required
82
- try:
83
- task = store.create_task(payload)
84
- except ValueError as exc:
85
- return {"isError": True, "content": str(exc)}
86
- run = None
87
- try:
88
- run = store.create_run("orchestration", task["title"], task_id=task["id"])
89
- store.update_task_status(task["id"], "queued")
90
- _enqueue({"kind": "orchestration", "run_id": run["id"], "task_id": task["id"]})
91
- except Exception as exc:
92
- # Mirror the HTTP creator: every partially-created task/run is closed
93
- # so MCP clients never receive a success while the UI shows queued.
94
- if run:
95
- try:
96
- store.update_run(run["id"], status="failed", error="任务启动失败,请稍后重试",
97
- ended_at=__import__("time").strftime("%Y-%m-%d %H:%M:%S"))
98
- except Exception:
99
- pass
100
- try:
101
- store.update_task_status(task["id"], "failed")
102
- except Exception:
103
- pass
104
- return {"isError": True, "content": "任务启动失败,请稍后重试"}
105
- return {"content": "已创建并排队:task_id=%s run_id=%s(可用 get_status 查进度)"
106
- % (task["id"], run["id"])}
107
-
108
-
109
- def _tool_get_status(args):
110
- from core import store
111
- task = store.get_task(str(args.get("task_id") or "").strip())
112
- if not task:
113
- return {"isError": True, "content": "任务不存在"}
114
- lines = ["任务:%s(%s)" % (task.get("title"), task.get("status"))]
115
- run = store.latest_run_by_task().get(task["id"])
116
- if run:
117
- lines.append("最近运行:%s(%s)" % (run.get("id"), run.get("status")))
118
- steps = [s for s in (run.get("steps") or []) if isinstance(s, dict)]
119
- if steps:
120
- cur = steps[-1]
121
- lines.append("步骤 %d/%d:%s(%s)" % (len(steps), run.get("total_steps") or len(steps),
122
- cur.get("title"), cur.get("status")))
123
- v = run.get("verdict") or {}
124
- if v.get("overall") is not None:
125
- lines.append("评分 %.1f(%s)" % (float(v["overall"]),
126
- "达标" if v.get("publishable") else "未达标"))
127
- if run.get("error"):
128
- lines.append("错误:%s" % str(run["error"])[:200])
129
- if task.get("git_state") == "isolated":
130
- lines.append("⚠ 任务分支待裁决(合并/丢弃)")
131
- return {"content": "\n".join(lines)}
132
-
133
-
134
- def _tool_list_recent(args):
135
- from core import store
136
- try:
137
- limit = max(1, min(30, int(args.get("limit") or 10)))
138
- except (TypeError, ValueError):
139
- limit = 10
140
- rows = [{"id": r.get("id"), "title": r.get("title"), "status": r.get("status"),
141
- "ts": r.get("started_at") or ""} for r in store.list_runs(limit=limit)]
142
- return {"content": json.dumps(rows, ensure_ascii=False, indent=1)}
143
-
144
-
145
- def _tool_usage_summary(args):
146
- from core import usage
147
- today, month = usage.cost_snapshot()
148
- tokens = sum(int(r.get("total") or 0) for r in usage._iter_records(31))
149
- return {"content": "今日花费 ¥%.2f\n本月花费 ¥%.2f\n近 31 天 tokens:%d(未标定单价的调用不计钱)"
150
- % (today, month, tokens)}
151
-
152
-
153
- def _tool_bench(args):
154
- from core import evalbench
155
- try:
156
- top = max(1, min(10, int(args.get("top") or 5)))
157
- except (TypeError, ValueError):
158
- top = 5
159
- board = evalbench.state().get("leaderboard") or []
160
- rows = [{"rank": r["rank"], "model": r["model"], "overall": r["overall"],
161
- "provider": r.get("provider_name")} for r in board[:top]]
162
- return {"content": json.dumps(rows, ensure_ascii=False, indent=1)}
163
-
164
-
165
- def _tool_contract(args):
166
- from core import contracts
167
- item = contracts.get(str(args.get("task_id") or "").strip())
168
- if not item:
169
- return {"isError": True, "content": "任务契约不存在"}
170
- return {"content": json.dumps(item, ensure_ascii=False, indent=1)}
171
-
172
-
173
- def _tool_evidence(args):
174
- from core import contracts
175
- try:
176
- item = contracts.add_evidence(str(args.get("task_id") or "").strip(), args,
177
- actor=str(args.get("actor") or "mcp")[:120])
178
- except ValueError as exc:
179
- return {"isError": True, "content": str(exc)}
180
- return {"content": json.dumps(item, ensure_ascii=False, indent=1)}
181
-
182
-
183
- def _tool_search(args):
184
- from core import retrieval
185
- try:
186
- rows = retrieval.search(args.get("query"), args.get("limit") or 10)
187
- except (TypeError, ValueError) as exc:
188
- return {"isError": True, "content": str(exc)}
189
- return {"content": json.dumps(rows, ensure_ascii=False, indent=1)}
190
-
191
-
192
- _TOOL_IMPL = {"create_task": _tool_create_task, "get_status": _tool_get_status,
193
- "list_recent": _tool_list_recent, "usage_summary": _tool_usage_summary,
194
- "bench_leaderboard": _tool_bench, "get_contract": _tool_contract,
195
- "record_evidence": _tool_evidence, "search_knowledge": _tool_search}
196
-
197
-
198
- def handle_request(msg):
199
- """处理一条客户端请求,返回响应 dict(通知返回 None)。"""
200
- if not isinstance(msg, dict) or msg.get("method") is None:
201
- return None
202
- rid = msg.get("id")
203
- method = str(msg["method"])
204
- if method == "initialize":
205
- return {"jsonrpc": "2.0", "id": rid, "result": {
206
- "protocolVersion": PROTOCOL_VERSION, "capabilities": {"tools": {}},
207
- "serverInfo": SERVER_INFO}}
208
- if method.startswith("notifications/"):
209
- return None
210
- if method == "tools/list":
211
- return {"jsonrpc": "2.0", "id": rid, "result": {"tools": TOOLS}}
212
- if method == "tools/call":
213
- params = msg.get("params") or {}
214
- name = str(params.get("name") or "")
215
- fn = _TOOL_IMPL.get(name)
216
- if fn is None:
217
- return {"jsonrpc": "2.0", "id": rid, "result": {
218
- "isError": True, "content": "未知工具: %s" % name}}
219
- try:
220
- out = fn(params.get("arguments") or {})
221
- except Exception as e:
222
- out = {"isError": True, "content": "工具执行失败: %s" % e}
223
- return {"jsonrpc": "2.0", "id": rid, "result": out}
224
- return {"jsonrpc": "2.0", "id": rid,
225
- "error": {"code": -32601, "message": "no such method: %s" % method}}
226
-
227
-
228
- def main():
229
- for raw in sys.stdin:
230
- line = raw.strip()
231
- if not line.startswith("{"):
232
- continue
233
- try:
234
- msg = json.loads(line)
235
- except Exception:
236
- continue
237
- resp = handle_request(msg)
238
- if resp is not None:
239
- sys.stdout.write(json.dumps(resp, ensure_ascii=False) + "\n")
240
- sys.stdout.flush()
241
-
242
-
243
- if __name__ == "__main__":
244
- 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
+ "acceptance_criteria": {"type": "array", "items": {"type": "string"}},
30
+ "approval_required": {"type": "boolean"}},
31
+ "required": ["goal"]}},
32
+ {"name": "get_status", "description": "查任务与最近一次运行的状态:进度、当前步骤、评分、错误。",
33
+ "inputSchema": {"type": "object", "properties": {
34
+ "task_id": {"type": "string", "description": "任务 ID"}}, "required": ["task_id"]}},
35
+ {"name": "list_recent", "description": "列出最近的运行(id/标题/状态/时间)。",
36
+ "inputSchema": {"type": "object", "properties": {
37
+ "limit": {"type": "integer", "description": "条数,默认 10,最大 30"}}}},
38
+ {"name": "usage_summary", "description": "用量台账汇总:今日/本月花费(¥)与 token 总量。",
39
+ "inputSchema": {"type": "object", "properties": {}}},
40
+ {"name": "bench_leaderboard", "description": "模型评测基准台能力榜(综合分 Top N)。",
41
+ "inputSchema": {"type": "object", "properties": {
42
+ "top": {"type": "integer", "description": "取前 N,默认 5"}}}},
43
+ {"name": "get_contract", "description": "读取任务验收标准、证据、审批和完成回执。",
44
+ "inputSchema": {"type": "object", "properties": {
45
+ "task_id": {"type": "string"}}, "required": ["task_id"]}},
46
+ {"name": "record_evidence", "description": "向任务契约追加结构化验收证据。",
47
+ "inputSchema": {"type": "object", "properties": {
48
+ "task_id": {"type": "string"}, "criterion": {"type": "string"},
49
+ "status": {"type": "string", "enum": ["passed", "failed", "unknown", "not_evaluated"]},
50
+ "summary": {"type": "string"}}, "required": ["task_id", "status"]}},
51
+ {"name": "search_knowledge", "description": "在本地检索索引中搜索带来源和命中词解释的上下文。",
52
+ "inputSchema": {"type": "object", "properties": {
53
+ "query": {"type": "string"}, "limit": {"type": "integer"}}, "required": ["query"]}},
54
+ ]
55
+
56
+
57
+ def _enqueue(job):
58
+ """thin wrapper:便于单测 mock(真链会拉起 worker 执行)。"""
59
+ from core import jobs
60
+ jobs.enqueue(job)
61
+
62
+
63
+ def _tool_create_task(args):
64
+ from core import settings as settings_mod, store
65
+ goal = str(args.get("goal") or "").strip()
66
+ if not goal:
67
+ return {"isError": True, "content": "goal 不能为空"}
68
+ payload = {"type": str(args.get("type") or "doc").strip()[:40] or "doc",
69
+ "title": (str(args.get("title") or "").strip() or goal[:30])[:40],
70
+ "goal": goal[:4000],
71
+ "workdir": str(args.get("workdir") or "").strip()
72
+ or settings_mod.default_workdir()}
73
+ criteria = args.get("acceptance_criteria")
74
+ if criteria is not None and not isinstance(criteria, list):
75
+ return {"isError": True, "content": "acceptance_criteria 必须是列表"}
76
+ if isinstance(criteria, list):
77
+ payload["acceptance_criteria"] = [str(x).strip()[:500] for x in criteria if str(x).strip()][:40]
78
+ approval_required = args.get("approval_required", False)
79
+ if not isinstance(approval_required, bool):
80
+ return {"isError": True, "content": "approval_required 必须是布尔值"}
81
+ payload["approval_required"] = approval_required
82
+ try:
83
+ task = store.create_task(payload)
84
+ except ValueError as exc:
85
+ return {"isError": True, "content": str(exc)}
86
+ run = None
87
+ try:
88
+ run = store.create_run("orchestration", task["title"], task_id=task["id"])
89
+ store.update_task_status(task["id"], "queued")
90
+ _enqueue({"kind": "orchestration", "run_id": run["id"], "task_id": task["id"]})
91
+ except Exception as exc:
92
+ # Mirror the HTTP creator: every partially-created task/run is closed
93
+ # so MCP clients never receive a success while the UI shows queued.
94
+ if run:
95
+ try:
96
+ store.update_run(run["id"], status="failed", error="任务启动失败,请稍后重试",
97
+ ended_at=__import__("time").strftime("%Y-%m-%d %H:%M:%S"))
98
+ except Exception:
99
+ pass
100
+ try:
101
+ store.update_task_status(task["id"], "failed")
102
+ except Exception:
103
+ pass
104
+ return {"isError": True, "content": "任务启动失败,请稍后重试"}
105
+ return {"content": "已创建并排队:task_id=%s run_id=%s(可用 get_status 查进度)"
106
+ % (task["id"], run["id"])}
107
+
108
+
109
+ def _tool_get_status(args):
110
+ from core import store
111
+ task = store.get_task(str(args.get("task_id") or "").strip())
112
+ if not task:
113
+ return {"isError": True, "content": "任务不存在"}
114
+ lines = ["任务:%s(%s)" % (task.get("title"), task.get("status"))]
115
+ run = store.latest_run_by_task().get(task["id"])
116
+ if run:
117
+ lines.append("最近运行:%s(%s)" % (run.get("id"), run.get("status")))
118
+ steps = [s for s in (run.get("steps") or []) if isinstance(s, dict)]
119
+ if steps:
120
+ cur = steps[-1]
121
+ lines.append("步骤 %d/%d:%s(%s)" % (len(steps), run.get("total_steps") or len(steps),
122
+ cur.get("title"), cur.get("status")))
123
+ v = run.get("verdict") or {}
124
+ if v.get("overall") is not None:
125
+ lines.append("评分 %.1f(%s)" % (float(v["overall"]),
126
+ "达标" if v.get("publishable") else "未达标"))
127
+ if run.get("error"):
128
+ lines.append("错误:%s" % str(run["error"])[:200])
129
+ if task.get("git_state") == "isolated":
130
+ lines.append("⚠ 任务分支待裁决(合并/丢弃)")
131
+ return {"content": "\n".join(lines)}
132
+
133
+
134
+ def _tool_list_recent(args):
135
+ from core import store
136
+ try:
137
+ limit = max(1, min(30, int(args.get("limit") or 10)))
138
+ except (TypeError, ValueError):
139
+ limit = 10
140
+ rows = [{"id": r.get("id"), "title": r.get("title"), "status": r.get("status"),
141
+ "ts": r.get("started_at") or ""} for r in store.list_runs(limit=limit)]
142
+ return {"content": json.dumps(rows, ensure_ascii=False, indent=1)}
143
+
144
+
145
+ def _tool_usage_summary(args):
146
+ from core import usage
147
+ today, month = usage.cost_snapshot()
148
+ tokens = sum(int(r.get("total") or 0) for r in usage._iter_records(31))
149
+ return {"content": "今日花费 ¥%.2f\n本月花费 ¥%.2f\n近 31 天 tokens:%d(未标定单价的调用不计钱)"
150
+ % (today, month, tokens)}
151
+
152
+
153
+ def _tool_bench(args):
154
+ from core import evalbench
155
+ try:
156
+ top = max(1, min(10, int(args.get("top") or 5)))
157
+ except (TypeError, ValueError):
158
+ top = 5
159
+ board = evalbench.state().get("leaderboard") or []
160
+ rows = [{"rank": r["rank"], "model": r["model"], "overall": r["overall"],
161
+ "provider": r.get("provider_name")} for r in board[:top]]
162
+ return {"content": json.dumps(rows, ensure_ascii=False, indent=1)}
163
+
164
+
165
+ def _tool_contract(args):
166
+ from core import contracts
167
+ item = contracts.get(str(args.get("task_id") or "").strip())
168
+ if not item:
169
+ return {"isError": True, "content": "任务契约不存在"}
170
+ return {"content": json.dumps(item, ensure_ascii=False, indent=1)}
171
+
172
+
173
+ def _tool_evidence(args):
174
+ from core import contracts
175
+ try:
176
+ item = contracts.add_evidence(str(args.get("task_id") or "").strip(), args,
177
+ actor=str(args.get("actor") or "mcp")[:120])
178
+ except ValueError as exc:
179
+ return {"isError": True, "content": str(exc)}
180
+ return {"content": json.dumps(item, ensure_ascii=False, indent=1)}
181
+
182
+
183
+ def _tool_search(args):
184
+ from core import retrieval
185
+ try:
186
+ rows = retrieval.search(args.get("query"), args.get("limit") or 10)
187
+ except (TypeError, ValueError) as exc:
188
+ return {"isError": True, "content": str(exc)}
189
+ return {"content": json.dumps(rows, ensure_ascii=False, indent=1)}
190
+
191
+
192
+ _TOOL_IMPL = {"create_task": _tool_create_task, "get_status": _tool_get_status,
193
+ "list_recent": _tool_list_recent, "usage_summary": _tool_usage_summary,
194
+ "bench_leaderboard": _tool_bench, "get_contract": _tool_contract,
195
+ "record_evidence": _tool_evidence, "search_knowledge": _tool_search}
196
+
197
+
198
+ def handle_request(msg):
199
+ """处理一条客户端请求,返回响应 dict(通知返回 None)。"""
200
+ if not isinstance(msg, dict) or msg.get("method") is None:
201
+ return None
202
+ rid = msg.get("id")
203
+ method = str(msg["method"])
204
+ if method == "initialize":
205
+ return {"jsonrpc": "2.0", "id": rid, "result": {
206
+ "protocolVersion": PROTOCOL_VERSION, "capabilities": {"tools": {}},
207
+ "serverInfo": SERVER_INFO}}
208
+ if method.startswith("notifications/"):
209
+ return None
210
+ if method == "tools/list":
211
+ return {"jsonrpc": "2.0", "id": rid, "result": {"tools": TOOLS}}
212
+ if method == "tools/call":
213
+ params = msg.get("params") or {}
214
+ name = str(params.get("name") or "")
215
+ fn = _TOOL_IMPL.get(name)
216
+ if fn is None:
217
+ return {"jsonrpc": "2.0", "id": rid, "result": {
218
+ "isError": True, "content": "未知工具: %s" % name}}
219
+ try:
220
+ out = fn(params.get("arguments") or {})
221
+ except Exception as e:
222
+ out = {"isError": True, "content": "工具执行失败: %s" % e}
223
+ return {"jsonrpc": "2.0", "id": rid, "result": out}
224
+ return {"jsonrpc": "2.0", "id": rid,
225
+ "error": {"code": -32601, "message": "no such method: %s" % method}}
226
+
227
+
228
+ def main():
229
+ for raw in sys.stdin:
230
+ line = raw.strip()
231
+ if not line.startswith("{"):
232
+ continue
233
+ try:
234
+ msg = json.loads(line)
235
+ except Exception:
236
+ continue
237
+ resp = handle_request(msg)
238
+ if resp is not None:
239
+ sys.stdout.write(json.dumps(resp, ensure_ascii=False) + "\n")
240
+ sys.stdout.flush()
241
+
242
+
243
+ if __name__ == "__main__":
244
+ main()