codebee 0.1.101 → 0.1.103
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 +14 -0
- package/LICENSE +21 -21
- package/README.md +3 -6
- package/app/.playwright-cli/console-2026-09-23T08-26-03-635Z.log +88 -0
- package/app/.playwright-cli/page-2026-09-23T08-26-03-845Z.yml +126 -0
- 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 +210 -0
- 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/core/acceptance.py +100 -100
- package/app/core/acp_client.py +457 -426
- package/app/core/aiflavor.py +197 -197
- package/app/core/analytics.py +38 -38
- 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/backend_profiles.py +188 -188
- package/app/core/backup.py +470 -470
- package/app/core/beekeeper.py +183 -183
- package/app/core/benchstore.py +102 -102
- package/app/core/board.py +21 -17
- package/app/core/bookmeta.py +1036 -1036
- package/app/core/bookmeta_catalog.py +180 -180
- package/app/core/branching.py +137 -137
- package/app/core/builtin_agent.py +13 -4
- package/app/core/capability.py +144 -144
- package/app/core/catalog.py +28 -6
- package/app/core/chaptersafe.py +32 -32
- package/app/core/checkpoints.py +117 -31
- package/app/core/cleanup.py +303 -303
- package/app/core/command_auth.py +302 -302
- package/app/core/compaction.py +201 -201
- package/app/core/contracts.py +255 -255
- 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 +1 -1
- 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/eval_matrix.py +91 -82
- package/app/core/evaluation.py +174 -174
- package/app/core/failure_notify.py +290 -290
- package/app/core/flow_graph.py +33 -33
- package/app/core/goal_service.py +167 -167
- package/app/core/health.py +389 -389
- package/app/core/history.py +38 -38
- package/app/core/hooks.py +162 -162
- package/app/core/knowledge_pipeline.py +86 -86
- package/app/core/manager.py +2385 -2385
- package/app/core/mcp_client.py +7 -2
- package/app/core/mocks.py +64 -64
- package/app/core/notify.py +201 -201
- package/app/core/novel_quality.py +181 -181
- package/app/core/openapi.py +217 -209
- package/app/core/operations.py +228 -228
- package/app/core/paihang.py +145 -145
- package/app/core/pipeline.py +6 -3
- package/app/core/policy.py +9 -0
- package/app/core/portguard.py +111 -111
- package/app/core/presence.py +77 -77
- package/app/core/preview.py +293 -293
- package/app/core/project_memory.py +206 -206
- package/app/core/prompt_guard.py +78 -78
- package/app/core/publish/__init__.py +7 -7
- package/app/core/publish/auto.py +659 -562
- package/app/core/publish/chaprows.py +138 -138
- package/app/core/publish/fanqie.py +16 -10
- package/app/core/publish/flow.py +29 -1
- package/app/core/publish/flows-fanqie-calibrated.json +442 -442
- package/app/core/publish/manager.py +1185 -1177
- package/app/core/publish/qimao.py +193 -193
- package/app/core/publish/volumes.py +108 -108
- package/app/core/publish/ws.py +139 -139
- package/app/core/quality_gate.py +201 -201
- package/app/core/redact.py +38 -38
- package/app/core/repeat_guard.py +129 -129
- package/app/core/retrieval.py +137 -137
- package/app/core/sandbox_runtime.py +95 -0
- 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.py +215 -215
- package/app/core/settings_schema.py +282 -282
- package/app/core/share_page.py +114 -114
- package/app/core/skill_scan.py +139 -139
- 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/step_runner.py +132 -132
- package/app/core/store.py +13 -3
- package/app/core/story_tracking.py +275 -275
- 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/tool_outputs.py +107 -107
- package/app/core/tracing.py +207 -207
- package/app/core/upstream.py +41 -41
- package/app/core/usage.py +1038 -1038
- package/app/core/volumes.py +389 -389
- package/app/core/webhooks.py +89 -89
- package/app/core/wxdigest.py +865 -865
- package/app/core/zentao.py +2692 -2692
- package/app/main.py +1 -0
- package/app/mcp_server.py +244 -244
- package/app/output/_createbook_body.json +1 -0
- package/app/output/_probe_fq_click.py +50 -0
- package/app/output/_probe_fq_form.py +42 -0
- package/app/output/_probe_fq_modal.py +55 -0
- package/app/output/_probe_fq_points.py +71 -0
- package/app/output/_probe_fq_submit.py +34 -0
- package/app/output/_probe_fq_watch.py +62 -0
- package/app/output/workbench-clean-preview.jpg +0 -0
- package/app/output/workbench-reference-render.png +0 -0
- package/app/output/workbench-shell.png +0 -0
- package/app/pick_dialog.py +110 -110
- package/app/ui/app.js +165 -29
- package/app/ui/artdiff.js +74 -74
- package/app/ui/hive3d.js +491 -223
- package/app/ui/i18n.js +18 -1
- package/app/ui/icons/bee.svg +79 -79
- package/app/ui/index.html +24 -4
- package/app/ui/manifest.json +16 -16
- package/app/ui/qrcode.js +2297 -2297
- package/app/ui/style.css +548 -18
- package/bin/tutti.js +147 -147
- package/package.json +1 -1
- package/tools/wx_reader.py +261 -261
package/app/main.py
CHANGED
|
@@ -2848,6 +2848,7 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
2848
2848
|
"thinking": s.get("thinking") or "",
|
|
2849
2849
|
"stream": (s.get("stream") or "") if running else "",
|
|
2850
2850
|
"activity": (s.get("activity") or []) if running else [],
|
|
2851
|
+
"acp_events": s.get("acp_events") or [],
|
|
2851
2852
|
"live": s.get("live") or 0,
|
|
2852
2853
|
})
|
|
2853
2854
|
|
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()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"platform": "fanqie", "auto_submit": true, "force": true, "force_confirmed": true, "force_reason": "用户指定重建:平台实核未建成,台账假登记已清"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""复现产品 real_click_text 点击「请选择作品标签」:列出全部候选元素+点击后弹层实况。只读。"""
|
|
3
|
+
import sys
|
|
4
|
+
import time
|
|
5
|
+
import json
|
|
6
|
+
|
|
7
|
+
sys.path.insert(0, r"E:\GoOut\MultiAgentOrchestration")
|
|
8
|
+
from app.core.publish import manager # noqa: E402
|
|
9
|
+
|
|
10
|
+
b, page = manager._open_page("fanqie")
|
|
11
|
+
page.navigate("https://fanqienovel.com/main/writer/create", timeout=30)
|
|
12
|
+
page.call("() => location.reload()")
|
|
13
|
+
time.sleep(5)
|
|
14
|
+
|
|
15
|
+
# 与 real_click_text 相同的候选定位,但列出全部命中而非只取第一个
|
|
16
|
+
r = page.call(
|
|
17
|
+
"(t,scope,y1)=>{"
|
|
18
|
+
"const vis=e=>e.getBoundingClientRect().width>0;"
|
|
19
|
+
"const els=[...document.querySelectorAll(scope)].filter(vis);"
|
|
20
|
+
"let cands=els.filter(e=>{const x=(e.innerText||'').trim();return x&&x.includes(t);});"
|
|
21
|
+
"cands=cands.filter(e=>e.getBoundingClientRect().y<=y1);"
|
|
22
|
+
"return cands.slice(0,12).map(e=>{const rc=e.getBoundingClientRect();"
|
|
23
|
+
"return {tag:e.tagName,cls:String(e.className).slice(0,60),"
|
|
24
|
+
"txt:(e.innerText||'').trim().slice(0,40),"
|
|
25
|
+
"x:Math.round(rc.x+rc.width/2),y:Math.round(rc.y+rc.height/2),w:Math.round(rc.width),h:Math.round(rc.height)};});"
|
|
26
|
+
"}", "请选择作品标签", "[class*=select-view],div,span", 430)
|
|
27
|
+
print("候选元素(y<=430):")
|
|
28
|
+
for c in r or []:
|
|
29
|
+
print(" ", json.dumps(c, ensure_ascii=False))
|
|
30
|
+
|
|
31
|
+
# 按产品同款规则(最小文本者胜)选出目标并真实点击
|
|
32
|
+
hit = page.real_click_text("请选择作品标签", "[class*=select-view],div,span", y_max=430)
|
|
33
|
+
print("real_click_text ->", hit)
|
|
34
|
+
time.sleep(2.5)
|
|
35
|
+
|
|
36
|
+
# 点击后弹层实况:类名探查放宽到全 DOM
|
|
37
|
+
info = page.call("""() => {
|
|
38
|
+
const fixed = [...document.querySelectorAll('body > div')]
|
|
39
|
+
.filter(d => d.getBoundingClientRect().width > 200)
|
|
40
|
+
.slice(0, 8);
|
|
41
|
+
return {
|
|
42
|
+
category_modal: !!document.querySelector('.category-modal'),
|
|
43
|
+
portals: fixed.map(d => ({
|
|
44
|
+
cls: String(d.className).slice(0, 70),
|
|
45
|
+
txt: (d.innerText || '').slice(0, 200).split('\\n').join('|')
|
|
46
|
+
})),
|
|
47
|
+
dushi: document.body.innerText.indexOf('都市脑洞') >= 0
|
|
48
|
+
};
|
|
49
|
+
}""")
|
|
50
|
+
print(json.dumps(info, ensure_ascii=False, indent=1))
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""dump 建书表单当前状态:各字段值+校验错误提示,定位提交失败字段。只读。"""
|
|
3
|
+
import sys
|
|
4
|
+
import time
|
|
5
|
+
import json
|
|
6
|
+
|
|
7
|
+
sys.path.insert(0, r"E:\GoOut\MultiAgentOrchestration")
|
|
8
|
+
from app.core.publish import manager # noqa: E402
|
|
9
|
+
|
|
10
|
+
b, page = manager._open_page("fanqie")
|
|
11
|
+
print("url:", page.url())
|
|
12
|
+
info = page.call("""() => {
|
|
13
|
+
const out = {errors: [], fields: []};
|
|
14
|
+
// arco 校验错误信息
|
|
15
|
+
[...document.querySelectorAll('.arco-form-message,[class*=error],[class*=message]')]
|
|
16
|
+
.filter(e => e.offsetParent && (e.innerText || '').trim())
|
|
17
|
+
.slice(0, 10)
|
|
18
|
+
.forEach(e => out.errors.push((e.innerText || '').trim().slice(0, 80)));
|
|
19
|
+
// 各输入控件现值
|
|
20
|
+
[...document.querySelectorAll('input[placeholder],textarea[placeholder]')]
|
|
21
|
+
.slice(0, 12)
|
|
22
|
+
.forEach(e => out.fields.push({
|
|
23
|
+
ph: e.placeholder, tag: e.tagName,
|
|
24
|
+
val: (e.value || '').slice(0, 60), len: (e.value || '').length}));
|
|
25
|
+
// contenteditable(简介可能是富文本)
|
|
26
|
+
[...document.querySelectorAll('[contenteditable=true]')]
|
|
27
|
+
.slice(0, 4)
|
|
28
|
+
.forEach(e => out.fields.push({
|
|
29
|
+
ph: '(contenteditable:' + String(e.className).slice(0, 30) + ')',
|
|
30
|
+
tag: 'CE', val: (e.innerText || '').slice(0, 60),
|
|
31
|
+
len: (e.innerText || '').length}));
|
|
32
|
+
out.body_tail = document.body.innerText.slice(-400).split('\\n').join('|');
|
|
33
|
+
return out;
|
|
34
|
+
}""")
|
|
35
|
+
print(json.dumps(info, ensure_ascii=False, indent=1))
|
|
36
|
+
|
|
37
|
+
# 对照 book_meta 的简介长度
|
|
38
|
+
meta = json.load(open(
|
|
39
|
+
r"C:\Users\HP\AppData\Roaming\codebee\tasks\t-20260924-215431-0715.json",
|
|
40
|
+
encoding="utf-8"))
|
|
41
|
+
s = (meta.get("book_meta", {}).get("fanqie", {}).get("data", {}).get("summary") or "")
|
|
42
|
+
print("book_meta summary 长度:", len(s))
|