codebee 0.1.74 → 0.1.77

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 (119) hide show
  1. package/CHANGELOG.md +628 -607
  2. package/LICENSE +21 -21
  3. package/README.md +507 -508
  4. package/app/.playwright-cli/console-2026-09-23T08-26-03-635Z.log +88 -0
  5. package/app/.playwright-cli/page-2026-09-23T08-26-03-845Z.yml +126 -0
  6. package/app/.playwright-cli/page-2026-09-23T08-29-44-203Z.png +0 -0
  7. package/app/.playwright-cli/page-2026-09-23T08-35-42-046Z.yml +210 -0
  8. package/app/.playwright-cli/page-2026-09-23T08-36-52-858Z.png +0 -0
  9. package/app/.playwright-cli/page-2026-09-23T08-37-33-938Z.png +0 -0
  10. package/app/.playwright-cli/run-detail-after.png +0 -0
  11. package/app/core/acceptance.py +100 -100
  12. package/app/core/aiflavor.py +197 -197
  13. package/app/core/analytics.py +38 -38
  14. package/app/core/artifacts.py +148 -148
  15. package/app/core/assets.py +110 -110
  16. package/app/core/attachments.py +623 -623
  17. package/app/core/audit.py +30 -30
  18. package/app/core/backup.py +470 -470
  19. package/app/core/beekeeper.py +183 -183
  20. package/app/core/benchstore.py +95 -95
  21. package/app/core/board.py +209 -209
  22. package/app/core/bookmeta.py +1036 -921
  23. package/app/core/bookmeta_catalog.py +180 -180
  24. package/app/core/branching.py +137 -137
  25. package/app/core/builtin_agent.py +1680 -1680
  26. package/app/core/capability.py +144 -144
  27. package/app/core/chaptersafe.py +32 -32
  28. package/app/core/cleanup.py +303 -303
  29. package/app/core/compaction.py +201 -201
  30. package/app/core/contracts.py +255 -255
  31. package/app/core/covergen.py +356 -356
  32. package/app/core/defectretro.py +43 -43
  33. package/app/core/diagnostics.py +114 -114
  34. package/app/core/dispatch.py +190 -190
  35. package/app/core/dispatch_log.py +128 -128
  36. package/app/core/env_scrub.py +85 -85
  37. package/app/core/errorlog.py +137 -137
  38. package/app/core/evaluation.py +174 -174
  39. package/app/core/failure_notify.py +290 -290
  40. package/app/core/goal_service.py +167 -167
  41. package/app/core/health.py +389 -387
  42. package/app/core/history.py +38 -38
  43. package/app/core/hooks.py +162 -162
  44. package/app/core/jobs.py +22 -0
  45. package/app/core/manager.py +2232 -2232
  46. package/app/core/mocks.py +64 -64
  47. package/app/core/notify.py +201 -201
  48. package/app/core/novel_quality.py +189 -189
  49. package/app/core/openapi.py +154 -154
  50. package/app/core/operations.py +228 -228
  51. package/app/core/paihang.py +145 -145
  52. package/app/core/pipeline.py +189 -18
  53. package/app/core/planner.py +88 -0
  54. package/app/core/portguard.py +111 -111
  55. package/app/core/presence.py +77 -77
  56. package/app/core/preview.py +293 -293
  57. package/app/core/prompt_guard.py +78 -78
  58. package/app/core/publish/__init__.py +7 -7
  59. package/app/core/publish/browser.py +655 -655
  60. package/app/core/publish/fanqie.py +165 -165
  61. package/app/core/publish/flow.py +640 -640
  62. package/app/core/publish/flows-fanqie-calibrated.json +336 -336
  63. package/app/core/publish/manager.py +1118 -1118
  64. package/app/core/publish/qimao.py +176 -176
  65. package/app/core/publish/ws.py +139 -139
  66. package/app/core/quality_gate.py +194 -194
  67. package/app/core/redact.py +38 -38
  68. package/app/core/repeat_guard.py +129 -129
  69. package/app/core/retrieval.py +137 -137
  70. package/app/core/router.py +398 -398
  71. package/app/core/runner.py +2238 -2225
  72. package/app/core/selfupdate.py +384 -384
  73. package/app/core/session_log.py +161 -161
  74. package/app/core/sessions.py +312 -312
  75. package/app/core/settings_schema.py +270 -270
  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/skills.py +1 -1
  85. package/app/core/step_runner.py +132 -132
  86. package/app/core/story_tracking.py +275 -241
  87. package/app/core/task_compile.py +199 -199
  88. package/app/core/telemetry.py +292 -292
  89. package/app/core/tlsctx.py +64 -64
  90. package/app/core/token_meter.py +147 -147
  91. package/app/core/tracing.py +144 -144
  92. package/app/core/upstream.py +41 -41
  93. package/app/core/usage.py +1035 -1035
  94. package/app/core/volumes.py +389 -389
  95. package/app/core/wxdigest.py +858 -858
  96. package/app/core/zentao.py +2692 -2692
  97. package/app/main.py +59 -0
  98. package/app/mcp_server.py +244 -244
  99. package/app/output/_createbook_body.json +1 -0
  100. package/app/output/_probe_fq_click.py +50 -0
  101. package/app/output/_probe_fq_form.py +42 -0
  102. package/app/output/_probe_fq_modal.py +55 -0
  103. package/app/output/_probe_fq_points.py +71 -0
  104. package/app/output/_probe_fq_submit.py +34 -0
  105. package/app/output/_probe_fq_watch.py +62 -0
  106. package/app/pick_dialog.py +110 -110
  107. package/app/ui/app.js +108 -12
  108. package/app/ui/artdiff.js +74 -74
  109. package/app/ui/board.html +162 -162
  110. package/app/ui/board.js +305 -305
  111. package/app/ui/i18n.js +15 -0
  112. package/app/ui/icons/bee.svg +79 -79
  113. package/app/ui/index.html +4 -0
  114. package/app/ui/manifest.json +16 -16
  115. package/app/ui/qrcode.js +2297 -2297
  116. package/app/ui/style.css +7086 -7070
  117. package/bin/tutti.js +147 -147
  118. package/package.json +48 -48
  119. package/tools/wx_reader.py +261 -261
package/app/main.py CHANGED
@@ -1096,6 +1096,9 @@ class Handler(BaseHTTPRequestHandler):
1096
1096
  m = re.match(r"^/api/tasks/([^/]+)/book-meta$", path)
1097
1097
  if m:
1098
1098
  return self._api_book_meta_generate(m.group(1))
1099
+ m = re.match(r"^/api/tasks/([^/]+)/book-meta/rename$", path)
1100
+ if m:
1101
+ return self._api_book_meta_rename(m.group(1))
1099
1102
  m = re.match(r"^/api/tasks/([^/]+)/cover$", path)
1100
1103
  if m:
1101
1104
  from core import covergen
@@ -1205,6 +1208,10 @@ class Handler(BaseHTTPRequestHandler):
1205
1208
  if err and not n:
1206
1209
  return self._json(400, {"error": err})
1207
1210
  return self._json(200, {"ok": True, "count": n, "skipped": skipped, "message": err})
1211
+ if path == "/api/runs/cancel_all":
1212
+ # 急停(kill switch):取消全部进行中任务。全量扫描不受侧栏
1213
+ # 窗口限制——窗口外的活跃运行也一并取消(hcom/amux 族竞品标配)。
1214
+ return self._json(200, {"ok": True, "count": jobs.cancel_all()})
1208
1215
  if path == "/api/runs/clear":
1209
1216
  n, skipped = store.clear_runs()
1210
1217
  return self._json(200, {"ok": True, "count": n, "skipped": skipped})
@@ -1953,6 +1960,58 @@ class Handler(BaseHTTPRequestHandler):
1953
1960
  args=(target_id, platform, author)).start()
1954
1961
  return self._json(200, {"ok": True, "started": True})
1955
1962
 
1963
+ def _api_book_meta_rename(self, task_id):
1964
+ """作品信息换名(POST /api/tasks/<id>/book-meta/rename,body: {platform})。
1965
+
1966
+ 只换 book_name 一个字段:整套重生成会覆盖已核对过的分类/标签/简介,
1967
+ 而用户的痛点只是书名与本站已有作品撞名。同步接口(单次轻模型调用),
1968
+ 成功才写盘,模型失败时现有数据原样不动。排除名单=全站已用书名
1969
+ (books.json 登记 + 各任务 book_meta),从根上防「换出来的还是重名」。
1970
+ 落点同生成:连载链根任务(2026-09-30 都是用第一个)。"""
1971
+ from core import bookmeta
1972
+ task = store.get_task(task_id)
1973
+ if not task:
1974
+ return self._json(404, {"error": "任务不存在"})
1975
+ body = self._body() or {}
1976
+ platform = (body.get("platform") or "").strip()
1977
+ if platform not in bookmeta.PLATFORMS:
1978
+ return self._json(400, {"error": "platform 必须是 fanqie 或 qimao"})
1979
+ chain = store.serial_chain_ids(task_id)
1980
+ target = (store.get_task(chain[0]) if chain else None) or task
1981
+ target_id = str(target.get("id"))
1982
+ if target.get("status") in ("queued", "running"):
1983
+ return self._json(400, {"error": "任务正在运行,请等本轮结束后再换名"})
1984
+ entry = ((target.get("book_meta") or {}).get(platform) or {})
1985
+ if entry.get("status") != "done" or not isinstance(entry.get("data"), dict):
1986
+ return self._json(400, {"error": "该平台作品信息尚未生成,没有书名可换"})
1987
+ old_name = str((entry["data"] or {}).get("book_name") or "").strip()
1988
+ if not old_name:
1989
+ return self._json(400, {"error": "现有记录没有书名可换"})
1990
+ new_name, err = bookmeta.rename_book(target, platform, old_name,
1991
+ bookmeta.used_book_names())
1992
+ if err:
1993
+ return self._json(400, {"error": err})
1994
+ data = dict(entry["data"])
1995
+ data["book_name"] = new_name
1996
+ payload = dict(entry)
1997
+ payload["data"] = data
1998
+ payload["at"] = time.strftime("%Y-%m-%d %H:%M:%S")
1999
+ payload["source"] = (str(entry.get("source") or "") +
2000
+ ";换名:《%s》→《%s》" % (old_name, new_name)).lstrip(";")
2001
+ if not store.set_book_meta(target_id, platform, payload):
2002
+ return self._json(404, {"error": "任务不存在"})
2003
+ # 归档 Markdown 同步(工作目录在场才写;source 只进归档不进 book_meta
2004
+ # 数据——数据里多的键会让 repair_existing 的幂等比较每次不等)
2005
+ try:
2006
+ fp = Path(target.get("workdir") or "") / bookmeta.PLATFORMS[platform]["file"]
2007
+ if fp.parent.is_dir():
2008
+ fp.write_text(bookmeta.render_markdown(
2009
+ target, platform, dict(data, source=payload["source"])),
2010
+ encoding="utf-8")
2011
+ except OSError:
2012
+ pass
2013
+ return self._json(200, {"ok": True, "old": old_name, "new": new_name})
2014
+
1956
2015
  def _api_publish_platform_op(self, platform, op):
1957
2016
  """平台会话操作:connect 开浏览器等扫码 / disconnect 关 / probe 探测表单。"""
1958
2017
  from core.publish import manager as pub
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))