codebee 0.1.95 → 0.1.97

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 (144) hide show
  1. package/CHANGELOG.md +733 -720
  2. package/LICENSE +21 -21
  3. package/README.md +516 -509
  4. package/app/core/acceptance.py +100 -100
  5. package/app/core/agent_context.py +118 -0
  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/backup.py +470 -470
  13. package/app/core/beekeeper.py +183 -183
  14. package/app/core/benchstore.py +102 -95
  15. package/app/core/board.py +246 -209
  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/builtin_agent.py +2055 -1680
  20. package/app/core/capability.py +144 -144
  21. package/app/core/chaptersafe.py +32 -32
  22. package/app/core/checkpoints.py +648 -0
  23. package/app/core/cleanup.py +303 -303
  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.py +198 -191
  30. package/app/core/dispatch_log.py +128 -128
  31. package/app/core/env_scrub.py +85 -85
  32. package/app/core/errorlog.py +137 -137
  33. package/app/core/eval_matrix.py +82 -0
  34. package/app/core/evalbench.py +184 -91
  35. package/app/core/evaluation.py +174 -174
  36. package/app/core/failure_notify.py +290 -290
  37. package/app/core/flow_graph.py +33 -0
  38. package/app/core/goal_service.py +167 -167
  39. package/app/core/health.py +389 -389
  40. package/app/core/history.py +38 -38
  41. package/app/core/hooks.py +162 -162
  42. package/app/core/jobs.py +25 -4
  43. package/app/core/knowledge_pipeline.py +86 -0
  44. package/app/core/manager.py +2232 -2232
  45. package/app/core/mcp_client.py +200 -29
  46. package/app/core/mocks.py +64 -64
  47. package/app/core/notify.py +201 -201
  48. package/app/core/novel_quality.py +181 -189
  49. package/app/core/openapi.py +177 -154
  50. package/app/core/operations.py +228 -228
  51. package/app/core/paihang.py +145 -145
  52. package/app/core/pipeline.py +319 -59
  53. package/app/core/planner.py +17 -11
  54. package/app/core/policy.py +84 -0
  55. package/app/core/portguard.py +111 -111
  56. package/app/core/presence.py +77 -77
  57. package/app/core/preview.py +293 -293
  58. package/app/core/project_memory.py +139 -0
  59. package/app/core/prompt_guard.py +78 -78
  60. package/app/core/publish/__init__.py +7 -7
  61. package/app/core/publish/auto.py +106 -22
  62. package/app/core/publish/browser.py +694 -655
  63. package/app/core/publish/chaprows.py +138 -0
  64. package/app/core/publish/fanqie.py +212 -165
  65. package/app/core/publish/flow.py +832 -640
  66. package/app/core/publish/flows-fanqie-calibrated.json +361 -336
  67. package/app/core/publish/ledger.py +51 -2
  68. package/app/core/publish/manager.py +1150 -1118
  69. package/app/core/publish/qimao.py +176 -176
  70. package/app/core/publish/volumes.py +108 -0
  71. package/app/core/publish/ws.py +139 -139
  72. package/app/core/quality_gate.py +194 -194
  73. package/app/core/redact.py +38 -38
  74. package/app/core/repeat_guard.py +129 -129
  75. package/app/core/retrieval.py +137 -137
  76. package/app/core/router.py +398 -398
  77. package/app/core/runner.py +2239 -2238
  78. package/app/core/selfupdate.py +384 -384
  79. package/app/core/session_log.py +161 -161
  80. package/app/core/sessions.py +312 -312
  81. package/app/core/settings.py +33 -5
  82. package/app/core/settings_schema.py +282 -271
  83. package/app/core/share_page.py +114 -114
  84. package/app/core/skill_scan.py +139 -139
  85. package/app/core/skillpacks/fanqie-novel.md +8 -6
  86. package/app/core/skillpacks/market/character-bible.md +66 -66
  87. package/app/core/skillpacks/market/code-risk-checklist.md +58 -58
  88. package/app/core/skillpacks/market/git-workflow.md +57 -57
  89. package/app/core/skillpacks/market/release-notes.md +72 -72
  90. package/app/core/skillpacks/market/weekly-report.md +71 -71
  91. package/app/core/skillpacks/market/worldview-consistency.md +70 -70
  92. package/app/core/skillpacks/qimao-signing.md +14 -11
  93. package/app/core/skills.py +32 -21
  94. package/app/core/step_runner.py +132 -132
  95. package/app/core/store.py +24 -0
  96. package/app/core/story_tracking.py +275 -275
  97. package/app/core/task_compile.py +199 -199
  98. package/app/core/telemetry.py +292 -292
  99. package/app/core/tlsctx.py +64 -64
  100. package/app/core/token_meter.py +147 -147
  101. package/app/core/tool_outputs.py +107 -0
  102. package/app/core/tracing.py +207 -144
  103. package/app/core/upstream.py +41 -41
  104. package/app/core/usage.py +1038 -1038
  105. package/app/core/volumes.py +389 -389
  106. package/app/core/webhooks.py +89 -0
  107. package/app/core/wxdigest.py +865 -858
  108. package/app/core/zentao.py +2692 -2692
  109. package/app/main.py +179 -10
  110. package/app/mcp_server.py +244 -244
  111. package/app/pick_dialog.py +110 -110
  112. package/app/ui/app.js +563 -21
  113. package/app/ui/artdiff.js +74 -74
  114. package/app/ui/assets/community-qr.png +0 -0
  115. package/app/ui/assets/donate-alipay.jpg +0 -0
  116. package/app/ui/assets/donate-qr.jpg +0 -0
  117. package/app/ui/assets/workbench-reference.png +0 -0
  118. package/app/ui/board.css +451 -666
  119. package/app/ui/board.html +165 -162
  120. package/app/ui/board.js +509 -305
  121. package/app/ui/hive3d.js +242 -0
  122. package/app/ui/i18n.js +125 -9
  123. package/app/ui/icons/bee.svg +79 -79
  124. package/app/ui/index.html +60 -3
  125. package/app/ui/manifest.json +16 -16
  126. package/app/ui/qrcode.js +2297 -2297
  127. package/app/ui/style.css +189 -9
  128. package/bin/tutti.js +147 -147
  129. package/package.json +49 -48
  130. package/tools/wx_reader.py +261 -261
  131. package/app/.playwright-cli/console-2026-09-23T08-26-03-635Z.log +0 -88
  132. package/app/.playwright-cli/page-2026-09-23T08-26-03-845Z.yml +0 -126
  133. package/app/.playwright-cli/page-2026-09-23T08-29-44-203Z.png +0 -0
  134. package/app/.playwright-cli/page-2026-09-23T08-35-42-046Z.yml +0 -210
  135. package/app/.playwright-cli/page-2026-09-23T08-36-52-858Z.png +0 -0
  136. package/app/.playwright-cli/page-2026-09-23T08-37-33-938Z.png +0 -0
  137. package/app/.playwright-cli/run-detail-after.png +0 -0
  138. package/app/output/_createbook_body.json +0 -1
  139. package/app/output/_probe_fq_click.py +0 -50
  140. package/app/output/_probe_fq_form.py +0 -42
  141. package/app/output/_probe_fq_modal.py +0 -55
  142. package/app/output/_probe_fq_points.py +0 -71
  143. package/app/output/_probe_fq_submit.py +0 -34
  144. package/app/output/_probe_fq_watch.py +0 -62
@@ -17,15 +17,20 @@ name 用于工具名前缀(mcp__<name>__<tool>),必须 [a-z0-9_-]。
17
17
  from __future__ import annotations
18
18
 
19
19
  import json
20
+ import hashlib
20
21
  import os
21
22
  import re
23
+ import shutil
22
24
  import subprocess
25
+ import sys
23
26
  import threading
24
27
  import time
28
+ from pathlib import Path
25
29
 
26
30
  _LOCK = threading.RLock()
27
31
  _TOOLS_CACHE = {} # server_key → (ts, tools)
28
32
  _TOOLS_TTL = 300.0
33
+ _TOOLS_CONFIG_SHA = ""
29
34
 
30
35
  MAX_SERVERS = 4
31
36
  MAX_MCP_TOOLS = 24 # 全部服务器合计注入内置智能体的工具数上限
@@ -39,8 +44,10 @@ _CLIENT_INFO = {"name": "CodeBee", "version": "1.0"}
39
44
 
40
45
  def invalidate_tools_cache():
41
46
  """服务器配置变化后丢弃已发现工具(插件启停等场景调用)。"""
47
+ global _TOOLS_CONFIG_SHA
42
48
  with _LOCK:
43
49
  _TOOLS_CACHE.clear()
50
+ _TOOLS_CONFIG_SHA = ""
44
51
 
45
52
 
46
53
  def parse_servers(text):
@@ -70,18 +77,86 @@ def parse_servers(text):
70
77
  args = [str(a) for a in args][:16]
71
78
  env = item.get("env") if isinstance(item.get("env"), dict) else {}
72
79
  env = {str(k): str(v) for k, v in list(env.items())[:16]}
73
- out.append({"name": name, "command": command, "args": args, "env": env})
80
+ disabled = item.get("disabled_tools")
81
+ if not isinstance(disabled, list):
82
+ disabled = []
83
+ disabled = list(dict.fromkeys(str(x).strip()[:120] for x in disabled
84
+ if str(x or "").strip()))[:200]
85
+ out.append({"name": name, "command": command, "args": args, "env": env,
86
+ "disabled_tools": disabled})
74
87
  return out, None
75
88
 
76
89
 
90
+ def _sandbox_launch(server, sandbox, workdir):
91
+ """Build an OS-isolated stdio server launch or refuse to spawn it.
92
+
93
+ MCP tools are arbitrary configured programs. On Linux they run under
94
+ bubblewrap with only declared roots mounted; platforms without an equivalent
95
+ backend fail closed. Runtime variables are synthetic/minimal unless explicitly
96
+ included in the task env allowlist.
97
+ """
98
+ from . import policy
99
+ if not sandbox or not workdir:
100
+ raise RuntimeError("MCP 调用缺少任务沙箱策略或工作目录,拒绝启动")
101
+ if os.name != "posix" or sys.platform == "darwin" or not shutil.which("bwrap"):
102
+ raise RuntimeError("MCP 服务尚无可用的 OS 隔离后端,拒绝启动")
103
+ root = Path(workdir).expanduser().resolve(strict=True)
104
+ normalized = policy.normalize_sandbox(sandbox, root)
105
+ roots = [Path(value).resolve(strict=True)
106
+ for value in normalized.get("allowed_roots") or []]
107
+ if not roots:
108
+ raise RuntimeError("MCP 沙箱没有允许目录,拒绝启动")
109
+ cwd = root if policy.path_allowed(root, normalized) else roots[0]
110
+ argv = _bubblewrap_argv(server, normalized, roots, cwd)
111
+ return argv, _sandbox_env(server, normalized), str(cwd)
112
+
113
+
114
+ def _sandbox_env(server, sandbox):
115
+ """Pass only the minimal runtime environment and explicitly allowed values."""
116
+ env = {"PATH": os.environ.get("PATH", "/usr/bin:/bin"),
117
+ "HOME": "/tmp", "TMPDIR": "/tmp"}
118
+ for name in sandbox.get("env_allowlist") or []:
119
+ if name in os.environ:
120
+ env[name] = os.environ[name]
121
+ elif name in (server.get("env") or {}):
122
+ env[name] = str(server["env"][name])
123
+ return env
124
+
125
+
126
+ def _bubblewrap_argv(server, sandbox, roots, cwd):
127
+ """Build bwrap argv from already validated paths (also host-testable)."""
128
+ argv = ["bwrap", "--die-with-parent", "--new-session", "--tmpfs", "/"]
129
+ for system_path in ("/usr", "/bin", "/sbin", "/lib", "/lib64", "/etc"):
130
+ if os.path.exists(system_path):
131
+ argv.extend(["--dir", system_path, "--ro-bind", system_path, system_path])
132
+ argv.extend(["--dir", "/proc", "--dir", "/dev", "--proc", "/proc",
133
+ "--dev", "/dev", "--tmpfs", "/tmp"])
134
+ if sandbox.get("network") is False:
135
+ argv.append("--unshare-net")
136
+ for allowed_root in roots:
137
+ for parent in reversed(allowed_root.parents):
138
+ if str(parent) != "/":
139
+ argv.extend(["--dir", str(parent)])
140
+ argv.extend(["--dir", str(allowed_root), "--bind",
141
+ str(allowed_root), str(allowed_root)])
142
+ argv.extend(["--chdir", str(cwd), "--", server["command"]]
143
+ + list(server.get("args") or []))
144
+ return argv
145
+
146
+
77
147
  class _Session:
78
148
  """一次性的 MCP 服务器会话:spawn → initialize → (list/call)→ 杀树。"""
79
149
 
80
- def __init__(self, server):
150
+ def __init__(self, server, sandbox=None, workdir=None, timeout_s=None):
81
151
  self.server = server
152
+ self.sandbox = sandbox
153
+ self.workdir = workdir
154
+ self.timeout_s = timeout_s
155
+ self.deadline = None
82
156
  self.proc = None
83
157
  self.lines = None # reader 线程产出的行队列
84
158
  self.err = None
159
+ self._reader_threads = []
85
160
 
86
161
  def _pump_err(self):
87
162
  try:
@@ -89,6 +164,11 @@ class _Session:
89
164
  pass
90
165
  except Exception:
91
166
  pass
167
+ finally:
168
+ try:
169
+ self.proc.stderr.close()
170
+ except Exception:
171
+ pass
92
172
 
93
173
  def _pump_out(self):
94
174
  try:
@@ -97,25 +177,48 @@ class _Session:
97
177
  except Exception:
98
178
  pass
99
179
  finally:
180
+ try:
181
+ self.proc.stdout.close()
182
+ except Exception:
183
+ pass
100
184
  self.lines.put(None) # EOF 哨兵
101
185
 
102
186
  def __enter__(self):
187
+ if self.timeout_s is not None:
188
+ self.deadline = time.monotonic() + max(0.0, float(self.timeout_s))
103
189
  try:
190
+ argv, env, cwd = _sandbox_launch(self.server, self.sandbox, self.workdir)
191
+ if self.deadline is not None and time.monotonic() >= self.deadline:
192
+ raise RuntimeError("MCP 任务时限已到,拒绝启动")
104
193
  self.proc = subprocess.Popen(
105
- [self.server["command"]] + list(self.server.get("args") or []),
194
+ argv,
106
195
  stdin=subprocess.PIPE, stdout=subprocess.PIPE,
107
196
  stderr=subprocess.PIPE, text=False,
108
- env={**os.environ, **(self.server.get("env") or {})})
197
+ env=env, cwd=cwd, start_new_session=True)
109
198
  except Exception as e:
110
199
  raise RuntimeError("MCP 服务器启动失败: %s" % e)
111
200
  import queue as _queue
112
201
  self.lines = _queue.Queue()
113
- threading.Thread(target=self._pump_out, daemon=True).start()
114
- threading.Thread(target=self._pump_err, daemon=True).start()
115
- self._request("initialize", {
116
- "protocolVersion": "2024-11-05", "capabilities": {},
117
- "clientInfo": _CLIENT_INFO}, timeout=_INIT_TIMEOUT)
118
- self._notify("notifications/initialized")
202
+ self._reader_threads = [
203
+ threading.Thread(target=self._pump_out, daemon=True),
204
+ threading.Thread(target=self._pump_err, daemon=True),
205
+ ]
206
+ for thread in self._reader_threads:
207
+ thread.start()
208
+ try:
209
+ init_timeout = _INIT_TIMEOUT
210
+ if self.deadline is not None:
211
+ init_timeout = min(init_timeout, self.deadline - time.monotonic())
212
+ if init_timeout <= 0:
213
+ raise RuntimeError("MCP 任务时限已到,初始化未执行")
214
+ self._request("initialize", {
215
+ "protocolVersion": "2024-11-05", "capabilities": {},
216
+ "clientInfo": _CLIENT_INFO}, timeout=init_timeout)
217
+ self._notify("notifications/initialized")
218
+ except Exception:
219
+ # Context manager __exit__ is not called when __enter__ raises.
220
+ self.__exit__(None, None, None)
221
+ raise
119
222
  return self
120
223
 
121
224
  def __exit__(self, *exc):
@@ -125,12 +228,44 @@ class _Session:
125
228
  self.proc.stdin.close()
126
229
  except Exception:
127
230
  pass
128
- finally:
129
231
  try:
130
- if self.proc:
232
+ if self.proc and self.proc.poll() is None:
233
+ # bwrap runs in its own process group; terminate descendants
234
+ # as well as the launcher so timed-out MCP servers cannot
235
+ # leave background children holding pipes or doing work.
236
+ try:
237
+ from . import runner
238
+ runner._kill_tree(self.proc.pid)
239
+ except Exception:
240
+ pass
131
241
  self.proc.kill()
132
242
  except Exception:
133
243
  pass
244
+ try:
245
+ if self.proc:
246
+ self.proc.wait(timeout=1)
247
+ except Exception:
248
+ pass
249
+ finally:
250
+ # Close all parent-side pipe handles even if initialization failed.
251
+ # Do not close a stream while its reader is blocked in read(): the
252
+ # buffered stream lock can make close wait forever if an escaped
253
+ # descendant still holds the pipe open. Let daemon readers finish
254
+ # on EOF and close only streams no thread currently owns.
255
+ for thread in self._reader_threads:
256
+ try:
257
+ thread.join(timeout=0.2)
258
+ except Exception:
259
+ pass
260
+ for name in ("stdout", "stderr"):
261
+ pipe = getattr(self.proc, name, None) if self.proc else None
262
+ readers_alive = any(
263
+ thread.is_alive() for thread in self._reader_threads)
264
+ if pipe and not readers_alive:
265
+ try:
266
+ pipe.close()
267
+ except Exception:
268
+ pass
134
269
  return False
135
270
 
136
271
  def _send(self, obj):
@@ -143,12 +278,17 @@ class _Session:
143
278
 
144
279
  def _request(self, method, params, timeout):
145
280
  """单请求单响应;忽略通知与未知 id 的服务端请求。"""
281
+ duration = max(0.0, float(timeout))
282
+ if self.deadline is not None:
283
+ duration = min(duration, self.deadline - time.monotonic())
284
+ if duration <= 0:
285
+ raise RuntimeError("%s 超时(任务时限已到)" % method)
146
286
  rid = id({})
147
287
  self._send({"jsonrpc": "2.0", "id": 1 if method == "initialize" else rid,
148
288
  "method": method, "params": params or {}})
149
- deadline = time.time() + max(1.0, timeout)
289
+ deadline = time.monotonic() + duration
150
290
  while True:
151
- remain = deadline - time.time()
291
+ remain = deadline - time.monotonic()
152
292
  if remain <= 0:
153
293
  raise RuntimeError("%s 超时(%.0fs)" % (method, timeout))
154
294
  try:
@@ -174,10 +314,10 @@ class _Session:
174
314
  return msg.get("result") or {}
175
315
 
176
316
 
177
- def list_tools(server):
317
+ def list_tools(server, *, sandbox=None, workdir=None):
178
318
  """发现服务器工具。返回 {"ok", "tools":[{"name","description","input_schema"}], "error"}。"""
179
319
  try:
180
- with _Session(server) as sess:
320
+ with _Session(server, sandbox=sandbox, workdir=workdir) as sess:
181
321
  res = sess._request("tools/list", {}, timeout=_INIT_TIMEOUT)
182
322
  tools = []
183
323
  for t in (res.get("tools") or [])[:16]:
@@ -192,11 +332,13 @@ def list_tools(server):
192
332
  return {"ok": False, "tools": [], "error": str(e)[:200]}
193
333
 
194
334
 
195
- def call_tool(server, tool_name, arguments, timeout_s=_CALL_TIMEOUT):
335
+ def call_tool(server, tool_name, arguments, timeout_s=_CALL_TIMEOUT,
336
+ *, sandbox=None, workdir=None):
196
337
  """调用一个工具。返回 {"ok", "text", "error", "is_error"}。"""
197
338
  try:
198
- timeout = min(_CALL_TIMEOUT_MAX, max(5.0, float(timeout_s or _CALL_TIMEOUT)))
199
- with _Session(server) as sess:
339
+ timeout = min(_CALL_TIMEOUT_MAX, max(0.001, float(timeout_s or _CALL_TIMEOUT)))
340
+ with _Session(server, sandbox=sandbox, workdir=workdir,
341
+ timeout_s=timeout) as sess:
200
342
  res = sess._request("tools/call",
201
343
  {"name": tool_name,
202
344
  "arguments": arguments if isinstance(arguments, dict) else {}},
@@ -216,46 +358,67 @@ def call_tool(server, tool_name, arguments, timeout_s=_CALL_TIMEOUT):
216
358
 
217
359
  # ---- 面向 builtin_agent 的合并视图(带缓存) ----
218
360
 
219
- def tool_specs_cached(force=False):
361
+ def tool_specs_cached(force=False, *, sandbox=None, workdir=None):
220
362
  """全部服务器的工具合并清单:[{server, name(原名), full_name, description,
221
363
  input_schema}]。TTL 内走缓存;单个服务器失败跳过(error 挂在条目外不打断)。"""
364
+ global _TOOLS_CONFIG_SHA
365
+ if not sandbox or not workdir:
366
+ return []
367
+ config_text = _settings_text()
368
+ config_sha = hashlib.sha256(config_text.encode("utf-8")).hexdigest()
369
+ scope_sha = hashlib.sha256(json.dumps({
370
+ "workdir": os.path.realpath(workdir), "sandbox": sandbox,
371
+ }, sort_keys=True, default=str).encode("utf-8")).hexdigest()
222
372
  with _LOCK:
373
+ if config_sha != _TOOLS_CONFIG_SHA:
374
+ _TOOLS_CACHE.clear()
375
+ _TOOLS_CONFIG_SHA = config_sha
223
376
  if not force:
224
377
  hit = True
225
- for key, (ts, _t) in _TOOLS_CACHE.items():
378
+ scoped = [(key, value) for key, value in _TOOLS_CACHE.items()
379
+ if isinstance(key, tuple) and key[0] == scope_sha]
380
+ for key, (ts, _t) in scoped:
226
381
  if time.time() - ts > _TOOLS_TTL:
227
382
  hit = False
228
383
  break
229
- if hit and _TOOLS_CACHE:
384
+ if hit and scoped:
230
385
  out = []
231
- for _key, (_ts, tools) in _TOOLS_CACHE.items():
386
+ for _key, (_ts, tools) in scoped:
232
387
  out.extend(tools)
233
388
  return out[:MAX_MCP_TOOLS]
234
- servers, err = parse_servers(_settings_text())
389
+ servers, err = parse_servers(config_text)
235
390
  if err or not servers:
236
391
  return []
392
+ from . import policy
393
+ disabled = set(policy.normalize_disabled_tools(
394
+ (sandbox or {}).get("disabled_tools")))
237
395
  out = []
238
396
  with _LOCK:
239
397
  _TOOLS_CACHE.clear()
240
398
  for srv in servers:
241
- res = list_tools(srv)
399
+ res = list_tools(srv, sandbox=sandbox, workdir=workdir)
242
400
  if not res.get("ok"):
243
401
  continue
244
402
  specs = []
245
403
  for t in res["tools"]:
404
+ full_name = "mcp__%s__%s" % (srv["name"], t["name"])
405
+ if (t["name"] in srv.get("disabled_tools", [])
406
+ or t["name"] in disabled or full_name in disabled):
407
+ continue
246
408
  specs.append({"server": srv["name"], "name": t["name"],
247
- "full_name": "mcp__%s__%s" % (srv["name"], t["name"]),
409
+ "full_name": full_name,
248
410
  "description": t["description"],
249
411
  "input_schema": t["input_schema"]})
250
412
  with _LOCK:
251
- _TOOLS_CACHE[srv["name"]] = (time.time(), specs)
413
+ _TOOLS_CACHE[(scope_sha, srv["name"])] = (time.time(), specs)
252
414
  out.extend(specs)
253
415
  if len(out) >= MAX_MCP_TOOLS:
254
416
  break
255
417
  return out[:MAX_MCP_TOOLS]
256
418
 
257
419
 
258
- def dispatch_full_name(full_name, arguments, timeout_s=_CALL_TIMEOUT):
420
+ def dispatch_full_name(full_name, arguments, timeout_s=_CALL_TIMEOUT, *,
421
+ sandbox=None, workdir=None, disabled_tools=None):
259
422
  """mcp__<server>__<tool> → 找服务器配置并调用。返回 {"ok","text","error"}。"""
260
423
  m = re.match(r"^mcp__([a-z0-9_-]{1,24})__(.+)$", str(full_name or ""))
261
424
  if not m:
@@ -267,7 +430,15 @@ def dispatch_full_name(full_name, arguments, timeout_s=_CALL_TIMEOUT):
267
430
  srv = next((s for s in servers if s["name"] == sname), None)
268
431
  if not srv:
269
432
  return {"ok": False, "text": "", "error": "MCP 服务器 %s 未配置" % sname}
270
- return call_tool(srv, tool, arguments, timeout_s=timeout_s)
433
+ if tool in (srv.get("disabled_tools") or []):
434
+ return {"ok": False, "text": "", "error": "MCP 工具已禁用: %s" % full_name}
435
+ if (full_name in set(str(x) for x in (disabled_tools or []))
436
+ or tool in set(str(x) for x in (disabled_tools or []))):
437
+ return {"ok": False, "text": "", "error": "任务策略已禁用 MCP 工具: %s" % full_name}
438
+ if sandbox is None or not workdir:
439
+ return {"ok": False, "text": "", "error": "MCP 调用缺少任务沙箱策略或工作目录,拒绝启动"}
440
+ return call_tool(srv, tool, arguments, timeout_s=timeout_s,
441
+ sandbox=sandbox, workdir=workdir)
271
442
 
272
443
 
273
444
  _SETTINGS_TEXT = None # main.py 启动注入(读 settings.mcp_servers)——本模块
package/app/core/mocks.py CHANGED
@@ -1,64 +1,64 @@
1
- # -*- coding: utf-8 -*-
2
- """内置演示/测试智能体(mock):不消耗任何配额即可跑通全流程。"""
3
- from __future__ import annotations
4
-
5
- import hashlib
6
-
7
-
8
- def _seed(agent_id):
9
- return int(hashlib.sha256(agent_id.encode("utf-8")).hexdigest()[:6], 16)
10
-
11
-
12
- def draft_manuscript(task, round_no):
13
- """生成一份示例稿件(mock 起草/修订直接落盘的内容)。"""
14
- title = task.get("title") or "未命名章节"
15
- goal = task.get("goal") or ""
16
- return (
17
- "# {title}\n\n"
18
- "这是第 {r} 轮 mock 草稿(演示用,不消耗模型配额)。\n\n"
19
- "## 任务目标\n{goal}\n\n"
20
- "## 正文\n"
21
- "夜色沿着山脊线缓慢退去,主角终于看清了石门上的刻痕——那不是文字,"
22
- "而是一段被反复涂抹又反复显影的名字。他伸手触碰的瞬间,耳边响起的"
23
- "却是自己昨夜说过的话。{extra}\n\n"
24
- "(本轮修订关注:{focus})\n"
25
- ).format(
26
- title=title, r=round_no, goal=goal,
27
- extra="这一段在第 2 轮修订中补充了人物动机的伏笔。" if round_no >= 2 else "",
28
- focus="节奏与悬念铺排" if round_no == 1 else "人物弧光与首尾呼应",
29
- )
30
-
31
-
32
- def implement_note(task):
33
- return "(mock 实现)已在目标目录写入 mock-impl.txt 作为变更占位。"
34
-
35
-
36
- def critique(agent_id, round_no, dims, threshold):
37
- """确定性评分:第 1 轮略低于阈值,第 2 轮达标,用于验证发布门禁逻辑。"""
38
- s = _seed(agent_id)
39
- scores = {}
40
- for i, d in enumerate(dims):
41
- jitter = ((s >> i) % 5) * 0.1
42
- scores[d] = round(min(9.5, 4.8 + 1.2 * round_no + jitter), 1)
43
- passed = all(v >= threshold for v in scores.values())
44
- issues = []
45
- if round_no == 1:
46
- issues = [
47
- {"dim": dims[0], "severity": "major", "note": "开篇信息密度偏低,钩子出现太晚(mock 意见)"},
48
- {"dim": dims[-1], "severity": "minor", "note": "段落长短交替不够,读感偏平(mock 意见)"},
49
- ]
50
- return {
51
- "scores": scores,
52
- "issues": issues,
53
- "summary": "mock 总评:第 %d 轮稿件%s发布阈值" % (round_no, "已达到" if passed else "尚未达到"),
54
- }
55
-
56
-
57
- def review(task, verify_passed):
58
- return {
59
- "pass": bool(verify_passed),
60
- "scores": {"正确性": 8.0 if verify_passed else 5.0, "可维护性": 8.0, "安全": 8.5},
61
- "issues": [] if verify_passed else [
62
- {"severity": "major", "title": "验证命令未通过", "detail": "请检查测试输出(mock 意见)"}],
63
- "summary": "mock 代码评审:%s" % ("通过" if verify_passed else "未通过"),
64
- }
1
+ # -*- coding: utf-8 -*-
2
+ """内置演示/测试智能体(mock):不消耗任何配额即可跑通全流程。"""
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+
7
+
8
+ def _seed(agent_id):
9
+ return int(hashlib.sha256(agent_id.encode("utf-8")).hexdigest()[:6], 16)
10
+
11
+
12
+ def draft_manuscript(task, round_no):
13
+ """生成一份示例稿件(mock 起草/修订直接落盘的内容)。"""
14
+ title = task.get("title") or "未命名章节"
15
+ goal = task.get("goal") or ""
16
+ return (
17
+ "# {title}\n\n"
18
+ "这是第 {r} 轮 mock 草稿(演示用,不消耗模型配额)。\n\n"
19
+ "## 任务目标\n{goal}\n\n"
20
+ "## 正文\n"
21
+ "夜色沿着山脊线缓慢退去,主角终于看清了石门上的刻痕——那不是文字,"
22
+ "而是一段被反复涂抹又反复显影的名字。他伸手触碰的瞬间,耳边响起的"
23
+ "却是自己昨夜说过的话。{extra}\n\n"
24
+ "(本轮修订关注:{focus})\n"
25
+ ).format(
26
+ title=title, r=round_no, goal=goal,
27
+ extra="这一段在第 2 轮修订中补充了人物动机的伏笔。" if round_no >= 2 else "",
28
+ focus="节奏与悬念铺排" if round_no == 1 else "人物弧光与首尾呼应",
29
+ )
30
+
31
+
32
+ def implement_note(task):
33
+ return "(mock 实现)已在目标目录写入 mock-impl.txt 作为变更占位。"
34
+
35
+
36
+ def critique(agent_id, round_no, dims, threshold):
37
+ """确定性评分:第 1 轮略低于阈值,第 2 轮达标,用于验证发布门禁逻辑。"""
38
+ s = _seed(agent_id)
39
+ scores = {}
40
+ for i, d in enumerate(dims):
41
+ jitter = ((s >> i) % 5) * 0.1
42
+ scores[d] = round(min(9.5, 4.8 + 1.2 * round_no + jitter), 1)
43
+ passed = all(v >= threshold for v in scores.values())
44
+ issues = []
45
+ if round_no == 1:
46
+ issues = [
47
+ {"dim": dims[0], "severity": "major", "note": "开篇信息密度偏低,钩子出现太晚(mock 意见)"},
48
+ {"dim": dims[-1], "severity": "minor", "note": "段落长短交替不够,读感偏平(mock 意见)"},
49
+ ]
50
+ return {
51
+ "scores": scores,
52
+ "issues": issues,
53
+ "summary": "mock 总评:第 %d 轮稿件%s发布阈值" % (round_no, "已达到" if passed else "尚未达到"),
54
+ }
55
+
56
+
57
+ def review(task, verify_passed):
58
+ return {
59
+ "pass": bool(verify_passed),
60
+ "scores": {"正确性": 8.0 if verify_passed else 5.0, "可维护性": 8.0, "安全": 8.5},
61
+ "issues": [] if verify_passed else [
62
+ {"severity": "major", "title": "验证命令未通过", "detail": "请检查测试输出(mock 意见)"}],
63
+ "summary": "mock 代码评审:%s" % ("通过" if verify_passed else "未通过"),
64
+ }