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.
- package/CHANGELOG.md +733 -720
- package/LICENSE +21 -21
- package/README.md +516 -509
- package/app/core/acceptance.py +100 -100
- package/app/core/agent_context.py +118 -0
- 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/backup.py +470 -470
- package/app/core/beekeeper.py +183 -183
- package/app/core/benchstore.py +102 -95
- package/app/core/board.py +246 -209
- 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 +2055 -1680
- package/app/core/capability.py +144 -144
- package/app/core/chaptersafe.py +32 -32
- package/app/core/checkpoints.py +648 -0
- package/app/core/cleanup.py +303 -303
- 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 +198 -191
- 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 +82 -0
- package/app/core/evalbench.py +184 -91
- package/app/core/evaluation.py +174 -174
- package/app/core/failure_notify.py +290 -290
- package/app/core/flow_graph.py +33 -0
- 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/jobs.py +25 -4
- package/app/core/knowledge_pipeline.py +86 -0
- package/app/core/manager.py +2232 -2232
- package/app/core/mcp_client.py +200 -29
- package/app/core/mocks.py +64 -64
- package/app/core/notify.py +201 -201
- package/app/core/novel_quality.py +181 -189
- package/app/core/openapi.py +177 -154
- package/app/core/operations.py +228 -228
- package/app/core/paihang.py +145 -145
- package/app/core/pipeline.py +319 -59
- package/app/core/planner.py +17 -11
- package/app/core/policy.py +84 -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 +139 -0
- package/app/core/prompt_guard.py +78 -78
- package/app/core/publish/__init__.py +7 -7
- package/app/core/publish/auto.py +106 -22
- package/app/core/publish/browser.py +694 -655
- package/app/core/publish/chaprows.py +138 -0
- package/app/core/publish/fanqie.py +212 -165
- package/app/core/publish/flow.py +832 -640
- package/app/core/publish/flows-fanqie-calibrated.json +361 -336
- package/app/core/publish/ledger.py +51 -2
- package/app/core/publish/manager.py +1150 -1118
- package/app/core/publish/qimao.py +176 -176
- package/app/core/publish/volumes.py +108 -0
- package/app/core/publish/ws.py +139 -139
- package/app/core/quality_gate.py +194 -194
- 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/router.py +398 -398
- package/app/core/runner.py +2239 -2238
- 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 +33 -5
- package/app/core/settings_schema.py +282 -271
- package/app/core/share_page.py +114 -114
- package/app/core/skill_scan.py +139 -139
- package/app/core/skillpacks/fanqie-novel.md +8 -6
- 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/skillpacks/qimao-signing.md +14 -11
- package/app/core/skills.py +32 -21
- package/app/core/step_runner.py +132 -132
- package/app/core/store.py +24 -0
- 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 -0
- package/app/core/tracing.py +207 -144
- 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 -0
- package/app/core/wxdigest.py +865 -858
- package/app/core/zentao.py +2692 -2692
- package/app/main.py +179 -10
- package/app/mcp_server.py +244 -244
- package/app/pick_dialog.py +110 -110
- package/app/ui/app.js +563 -21
- package/app/ui/artdiff.js +74 -74
- package/app/ui/assets/community-qr.png +0 -0
- package/app/ui/assets/donate-alipay.jpg +0 -0
- package/app/ui/assets/donate-qr.jpg +0 -0
- package/app/ui/assets/workbench-reference.png +0 -0
- package/app/ui/board.css +451 -666
- package/app/ui/board.html +165 -162
- package/app/ui/board.js +509 -305
- package/app/ui/hive3d.js +242 -0
- package/app/ui/i18n.js +125 -9
- package/app/ui/icons/bee.svg +79 -79
- package/app/ui/index.html +60 -3
- package/app/ui/manifest.json +16 -16
- package/app/ui/qrcode.js +2297 -2297
- package/app/ui/style.css +189 -9
- package/bin/tutti.js +147 -147
- package/package.json +49 -48
- package/tools/wx_reader.py +261 -261
- package/app/.playwright-cli/console-2026-09-23T08-26-03-635Z.log +0 -88
- package/app/.playwright-cli/page-2026-09-23T08-26-03-845Z.yml +0 -126
- package/app/.playwright-cli/page-2026-09-23T08-29-44-203Z.png +0 -0
- package/app/.playwright-cli/page-2026-09-23T08-35-42-046Z.yml +0 -210
- package/app/.playwright-cli/page-2026-09-23T08-36-52-858Z.png +0 -0
- package/app/.playwright-cli/page-2026-09-23T08-37-33-938Z.png +0 -0
- package/app/.playwright-cli/run-detail-after.png +0 -0
- package/app/output/_createbook_body.json +0 -1
- package/app/output/_probe_fq_click.py +0 -50
- package/app/output/_probe_fq_form.py +0 -42
- package/app/output/_probe_fq_modal.py +0 -55
- package/app/output/_probe_fq_points.py +0 -71
- package/app/output/_probe_fq_submit.py +0 -34
- package/app/output/_probe_fq_watch.py +0 -62
package/app/core/mcp_client.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
194
|
+
argv,
|
|
106
195
|
stdin=subprocess.PIPE, stdout=subprocess.PIPE,
|
|
107
196
|
stderr=subprocess.PIPE, text=False,
|
|
108
|
-
env=
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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.
|
|
289
|
+
deadline = time.monotonic() + duration
|
|
150
290
|
while True:
|
|
151
|
-
remain = deadline - 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(
|
|
199
|
-
with _Session(server
|
|
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
|
-
|
|
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
|
|
384
|
+
if hit and scoped:
|
|
230
385
|
out = []
|
|
231
|
-
for _key, (_ts, tools) in
|
|
386
|
+
for _key, (_ts, tools) in scoped:
|
|
232
387
|
out.extend(tools)
|
|
233
388
|
return out[:MAX_MCP_TOOLS]
|
|
234
|
-
servers, err = parse_servers(
|
|
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":
|
|
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
|
-
|
|
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
|
+
}
|