@hupan56/wlkj 3.3.13 → 3.3.14
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/bin/cli.js +1 -23
- package/package.json +29 -29
- package/templates/qoder/agents/design-agent.md +20 -0
- package/templates/qoder/agents/spec-generator.md +21 -0
- package/templates/qoder/commands/optional/wl-spec.md +2 -12
- package/templates/qoder/commands/optional/wl-status.md +0 -8
- package/templates/qoder/commands/wl-code.md +0 -8
- package/templates/qoder/commands/wl-commit.md +1 -11
- package/templates/qoder/commands/wl-init.md +129 -129
- package/templates/qoder/commands/wl-prd.md +0 -24
- package/templates/qoder/commands/wl-search.md +11 -26
- package/templates/qoder/commands/wl-task.md +1 -11
- package/templates/qoder/commands/wl-test.md +0 -8
- package/templates/qoder/config.yaml +8 -3
- package/templates/qoder/hooks/session-start.py +365 -384
- package/templates/qoder/hooks/stop-eval.py +254 -383
- package/templates/qoder/scripts/capability/adapters/mcp.py +314 -322
- package/templates/qoder/scripts/capability/registry.py +248 -245
- package/templates/qoder/scripts/capability/registry_mcp.py +4 -10
- package/templates/qoder/scripts/capability/smoke_test_report.json.new +94 -0
- package/templates/qoder/scripts/deployment/setup/init_doctor.py +2 -6
- package/templates/qoder/scripts/deployment/setup/install_qoderwork.py +14 -0
- package/templates/qoder/scripts/deployment/setup/setup.py +2 -16
- package/templates/qoder/scripts/domain/integration/return_to_platform.py +2 -4
- package/templates/qoder/scripts/domain/integration/spec_upload.py +4 -4
- package/templates/qoder/scripts/domain/kg/build/build_entity_registry.py +12 -12
- package/templates/qoder/scripts/domain/kg/build/build_relations.py +12 -12
- package/templates/qoder/scripts/domain/kg/extract/asset/__init__.py +10 -0
- package/templates/qoder/scripts/domain/kg/extract/asset/asset_tree.py +57 -0
- package/templates/qoder/scripts/domain/kg/extract/asset/discussion_importer.py +62 -0
- package/templates/qoder/scripts/domain/kg/extract/asset/prd_importer.py +146 -0
- package/templates/qoder/scripts/domain/kg/extract/asset/prototype_importer.py +64 -0
- package/templates/qoder/scripts/domain/kg/extract/asset/returns_importer.py +52 -0
- package/templates/qoder/scripts/domain/kg/extract/build_goal3.py +104 -0
- package/templates/qoder/scripts/domain/kg/extract/build_goal4.py +55 -0
- package/templates/qoder/scripts/domain/kg/extract/build_goal5.py +95 -0
- package/templates/qoder/scripts/domain/kg/extract/db/__init__.py +8 -0
- package/templates/qoder/scripts/domain/kg/extract/db/data_profile.py +22 -0
- package/templates/qoder/scripts/domain/kg/extract/db/fk_extractor.py +55 -0
- package/templates/qoder/scripts/domain/kg/extract/db/schema_extractor.py +90 -0
- package/templates/qoder/scripts/domain/kg/extract/inference/__init__.py +9 -0
- package/templates/qoder/scripts/domain/kg/extract/inference/community_summarizer.py +206 -0
- package/templates/qoder/scripts/domain/kg/extract/inference/embed_builder.py +132 -0
- package/templates/qoder/scripts/domain/kg/extract/inference/naming_matcher.py +80 -0
- package/templates/qoder/scripts/domain/kg/extract/inference/promote.py +59 -0
- package/templates/qoder/scripts/domain/kg/extract/inference/recompute.py +93 -0
- package/templates/qoder/scripts/domain/kg/extract/inference/weak_link.py +421 -0
- package/templates/qoder/scripts/domain/kg/extract/mybatis/__init__.py +9 -0
- package/templates/qoder/scripts/domain/kg/extract/mybatis/all.py +79 -0
- package/templates/qoder/scripts/domain/kg/extract/mybatis/mapper_parser.py +99 -0
- package/templates/qoder/scripts/domain/kg/extract/mybatis/relation_builder.py +69 -0
- package/templates/qoder/scripts/domain/kg/extract/mybatis/sql_extractor.py +78 -0
- package/templates/qoder/scripts/domain/kg/extract/prd/__init__.py +8 -0
- package/templates/qoder/scripts/domain/kg/extract/prd/prd_chunk_embed.py +105 -0
- package/templates/qoder/scripts/domain/kg/extract/prd/prd_llm_extract.py +153 -0
- package/templates/qoder/scripts/domain/kg/extract/prd/req_anchor.py +120 -0
- package/templates/qoder/scripts/domain/kg/graph/kg_link_db.py +235 -0
- package/templates/qoder/scripts/domain/kg/graph/kg_semantic.py +5 -41
- package/templates/qoder/scripts/domain/kg/kg.py +0 -14
- package/templates/qoder/scripts/domain/kg/search/enrich_prompt.py +238 -0
- package/templates/qoder/scripts/domain/kg/server/perf_bench.py +197 -0
- package/templates/qoder/scripts/domain/kg/switch_project.py +2 -2
- package/templates/qoder/scripts/domain/kg/sync_repowiki.py +109 -0
- package/templates/qoder/scripts/domain/task/wlkj_panel.py +1348 -1503
- package/templates/qoder/scripts/engine/poller.py +219 -0
- package/templates/qoder/scripts/foundation/bootstrap.py +145 -156
- package/templates/qoder/scripts/orchestration/wlkj.py +189 -306
- package/templates/qoder/settings.json +0 -8
- package/templates/qoder/skills/wl-spec/SKILL.md +1 -1
- package/templates/qoder/commands/wl-data.md +0 -46
- package/templates/qoder/commands/wl-fix.md +0 -47
- package/templates/qoder/commands/wl-knowledge.md +0 -41
- package/templates/qoder/commands/wl-review.md +0 -49
- package/templates/qoder/contracts/insight.md +0 -55
- package/templates/qoder/hooks/pre-tool-use-commit.py +0 -124
- package/templates/qoder/scripts/foundation/io/context_cache.py +0 -94
- package/templates/qoder/scripts/tool_guide.md +0 -70
- package/templates/qoder/scripts/validation/eval/alignment_matrix.py +0 -176
- package/templates/qoder/scripts/validation/eval/bf2_content_fidelity.py +0 -110
- package/templates/qoder/scripts/validation/eval/bf2_llmjudge.py +0 -104
- package/templates/qoder/scripts/validation/eval/bf_score.py +0 -218
- package/templates/qoder/scripts/validation/eval/code_flywheel.py +0 -150
- package/templates/qoder/scripts/validation/eval/dispatcher_ab.py +0 -156
- package/templates/qoder/scripts/validation/eval/dispatcher_ab_2026-07-21.json +0 -23
- package/templates/qoder/scripts/validation/eval/feature_fidelity_flywheel.py +0 -143
- package/templates/qoder/scripts/validation/eval/gradient_matrix.py +0 -261
- package/templates/qoder/scripts/validation/eval/gradient_matrix_baseline_2026-07-21.json +0 -33
- package/templates/qoder/scripts/validation/eval/metrics_dashboard.py +0 -105
- package/templates/qoder/scripts/validation/eval/multi_turn_flywheel.py +0 -118
- package/templates/qoder/scripts/validation/eval/prd_fidelity_flywheel.py +0 -128
- package/templates/qoder/scripts/validation/eval/prd_flywheel.py +0 -148
- package/templates/qoder/scripts/validation/eval/prototype_fidelity_flywheel.py +0 -166
- package/templates/qoder/scripts/validation/eval/recall_flywheel.py +0 -148
- package/templates/qoder/scripts/validation/eval/robustness_flywheel.py +0 -139
- package/templates/qoder/scripts/validation/eval/speed_accuracy_flywheel.py +0 -188
- package/templates/qoder/scripts/validation/eval/task_flywheel.py +0 -124
- package/templates/qoder/scripts/validation/eval/token_flywheel.py +0 -88
- package/templates/qoder/scripts/validation/metrics/eval_code_ac.py +0 -177
- package/templates/qoder/scripts/validation/metrics/lint_cases.py +0 -170
- package/templates/qoder/scripts/validation/test/test_context_cache.py +0 -78
- package/templates/qoder/scripts/validation/test/test_lint_cases.py +0 -60
- package/templates/qoder/scripts/validation/test/test_pre_tool_use_commit.py +0 -70
- package/templates/workspace/specs/REQ-TEST-CTX/.context-cache.json +0 -1
|
@@ -1,322 +1,314 @@
|
|
|
1
|
-
# -*- coding: utf-8 -*-
|
|
2
|
-
"""
|
|
3
|
-
adapters.mcp —— L1 MCP 能力统一入口(已接通 transport 层)。
|
|
4
|
-
|
|
5
|
-
设计模式: Adapter + Facade
|
|
6
|
-
|
|
7
|
-
职责:
|
|
8
|
-
把 McpRegistry(tool→server→transport 路由)适配成 McpCap 的
|
|
9
|
-
has_tool/call/list_tools 接口,供 Nexus Facade 统一暴露。
|
|
10
|
-
|
|
11
|
-
L1 能力定位:禅道/知识图谱/MySQL/蓝湖/playwright 已 MCP 化,
|
|
12
|
-
通过本入口跨宿主统一调用,复用已有 MCP 资产,绝不重写它们。
|
|
13
|
-
|
|
14
|
-
跨宿主行为:
|
|
15
|
-
- QoderWork / 任意宿主:通过 mcp_launcher spawn 本地 MCP server(stdio transport)
|
|
16
|
-
- 未来服务器:改 McpRegistry.SERVERS 配置 kind=http + url,工作流零改动
|
|
17
|
-
- 无可用 server:available=False,has_tool 返回 False(静默回退)
|
|
18
|
-
|
|
19
|
-
McpMemoryProvider:memory 职责链的 MCP provider,接通 kg.duckdb learning_events 表。
|
|
20
|
-
当 mem0/Zep MCP server 更成熟时可替换为 MCP 版本,接口不变。
|
|
21
|
-
"""
|
|
22
|
-
from __future__ import annotations
|
|
23
|
-
|
|
24
|
-
import
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
from
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
#
|
|
53
|
-
#
|
|
54
|
-
#
|
|
55
|
-
#
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
#
|
|
74
|
-
#
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
cli_text
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
#
|
|
102
|
-
#
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
#
|
|
106
|
-
#
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
"
|
|
110
|
-
"
|
|
111
|
-
"
|
|
112
|
-
"
|
|
113
|
-
"
|
|
114
|
-
"
|
|
115
|
-
"
|
|
116
|
-
"
|
|
117
|
-
"
|
|
118
|
-
"
|
|
119
|
-
"
|
|
120
|
-
"
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
"
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
"
|
|
127
|
-
"
|
|
128
|
-
"
|
|
129
|
-
"
|
|
130
|
-
"
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
#
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
if
|
|
154
|
-
cli_args.append(str(
|
|
155
|
-
if "
|
|
156
|
-
cli_args.
|
|
157
|
-
if "
|
|
158
|
-
cli_args.extend(["--
|
|
159
|
-
if "
|
|
160
|
-
cli_args.append(str(args["
|
|
161
|
-
if "
|
|
162
|
-
cli_args.append(str(args["
|
|
163
|
-
if "
|
|
164
|
-
cli_args.
|
|
165
|
-
if "
|
|
166
|
-
cli_args.append(str(args["
|
|
167
|
-
if "
|
|
168
|
-
cli_args.append(str(args["
|
|
169
|
-
if "
|
|
170
|
-
cli_args.
|
|
171
|
-
if "
|
|
172
|
-
cli_args.
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
try:
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
"""
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
data = _json.loads(text) if text else {}
|
|
316
|
-
items = data.get("items", []) if isinstance(data, dict) else []
|
|
317
|
-
return [{"event": it.get("category", ""), "data": it.get("pattern", ""),
|
|
318
|
-
"ts": it.get("last_seen", ""), "dev": ",".join(it.get("source_devs", []) or [])}
|
|
319
|
-
for it in items[:limit]]
|
|
320
|
-
except Exception:
|
|
321
|
-
return []
|
|
322
|
-
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
adapters.mcp —— L1 MCP 能力统一入口(已接通 transport 层)。
|
|
4
|
+
|
|
5
|
+
设计模式: Adapter + Facade
|
|
6
|
+
|
|
7
|
+
职责:
|
|
8
|
+
把 McpRegistry(tool→server→transport 路由)适配成 McpCap 的
|
|
9
|
+
has_tool/call/list_tools 接口,供 Nexus Facade 统一暴露。
|
|
10
|
+
|
|
11
|
+
L1 能力定位:禅道/知识图谱/MySQL/蓝湖/playwright 已 MCP 化,
|
|
12
|
+
通过本入口跨宿主统一调用,复用已有 MCP 资产,绝不重写它们。
|
|
13
|
+
|
|
14
|
+
跨宿主行为:
|
|
15
|
+
- QoderWork / 任意宿主:通过 mcp_launcher spawn 本地 MCP server(stdio transport)
|
|
16
|
+
- 未来服务器:改 McpRegistry.SERVERS 配置 kind=http + url,工作流零改动
|
|
17
|
+
- 无可用 server:available=False,has_tool 返回 False(静默回退)
|
|
18
|
+
|
|
19
|
+
McpMemoryProvider:memory 职责链的 MCP provider,接通 kg.duckdb learning_events 表。
|
|
20
|
+
当 mem0/Zep MCP server 更成熟时可替换为 MCP 版本,接口不变。
|
|
21
|
+
"""
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
from typing import Any, Optional
|
|
25
|
+
|
|
26
|
+
from ..registry_mcp import McpRegistry, _load_servers
|
|
27
|
+
from protocol.transports import ToolResult
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# 单例 Registry(同一进程复用进程池,避免重复 spawn)
|
|
31
|
+
_REGISTRY: Optional[McpRegistry] = None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _get_registry() -> McpRegistry:
|
|
35
|
+
global _REGISTRY
|
|
36
|
+
if _REGISTRY is None:
|
|
37
|
+
_REGISTRY = McpRegistry()
|
|
38
|
+
return _REGISTRY
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class McpCap:
|
|
42
|
+
"""L1 MCP 能力统一入口 —— 复用已有 MCP server(禅道/知识图谱/MySQL/蓝湖)。
|
|
43
|
+
|
|
44
|
+
工作流调用:
|
|
45
|
+
cap = resolve()
|
|
46
|
+
result = cap.mcp.call("search_code", {"keyword": "保险"})
|
|
47
|
+
# 自动路由:MCP 可用走 MCP,不可用走 wlkj.py(CLI 降级)
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
def __init__(self, mcp_servers=None):
|
|
51
|
+
# ★ 空 mcp_servers (= [] 或 None) 视为"不限制 = 用全部本地 server"。
|
|
52
|
+
# 原版: mcp_servers=[] (空列表, is not None) → filtered 成空 → _servers={} → 0 工具,
|
|
53
|
+
# 整个 cap.mcp 自废武功。但本地 stdio transport (StdioTransport) 通过 mcp_launcher
|
|
54
|
+
# spawn 子进程, 不依赖宿主是否原生支持 MCP —— CLI / Codex / Claude 同样能起 server。
|
|
55
|
+
# 故: 宿主未声明 mcp_servers 或声明为空 = "没有宿主原生限制, 用全部本地可起的"。
|
|
56
|
+
if mcp_servers: # 非空列表才过滤
|
|
57
|
+
# 用 _load_servers() 而非硬编码 SERVERS —— 这样 mcp_config.json 改配置才生效
|
|
58
|
+
all_servers = _load_servers()
|
|
59
|
+
filtered = {k: v for k, v in all_servers.items() if k in mcp_servers}
|
|
60
|
+
self._registry = McpRegistry(servers=filtered)
|
|
61
|
+
else:
|
|
62
|
+
self._registry = _get_registry()
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def available(self) -> bool:
|
|
66
|
+
"""是否有任一 MCP server 可用。"""
|
|
67
|
+
try:
|
|
68
|
+
return self._registry.available()
|
|
69
|
+
except Exception:
|
|
70
|
+
return False
|
|
71
|
+
|
|
72
|
+
# ------------------------------------------------------------------
|
|
73
|
+
# McpCap 接口
|
|
74
|
+
# ------------------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
def has_tool(self, name: str) -> bool:
|
|
77
|
+
"""某 MCP 工具是否可用(跨所有已配置 server 查找)。"""
|
|
78
|
+
try:
|
|
79
|
+
return self._registry.has_tool(name)
|
|
80
|
+
except Exception:
|
|
81
|
+
return False
|
|
82
|
+
|
|
83
|
+
def call(self, name: str, args: Optional[dict] = None,
|
|
84
|
+
server: Optional[str] = None) -> ToolResult:
|
|
85
|
+
"""调用 MCP 工具(自动 CLI 降级)。
|
|
86
|
+
|
|
87
|
+
优先走 MCP;MCP 不可用时自动降级到 wlkj.py CLI 脚本。
|
|
88
|
+
工作流永远写 cap.mcp.call("tool_name", args),零宿主感知。
|
|
89
|
+
"""
|
|
90
|
+
# 1. MCP 可用 → 走 MCP
|
|
91
|
+
result = self._registry.call(name, args, server=server)
|
|
92
|
+
if not result.is_error:
|
|
93
|
+
return result
|
|
94
|
+
# 2. CLI 降级(MCP 不可用或调用失败)
|
|
95
|
+
cli_text = self._cli_fallback(name, args or {})
|
|
96
|
+
if cli_text is not None:
|
|
97
|
+
return ToolResult(text=cli_text)
|
|
98
|
+
return result # 无降级映射,返回原始错误
|
|
99
|
+
|
|
100
|
+
# ------------------------------------------------------------------
|
|
101
|
+
# CLI 降级(MCP 不可用时走 wlkj.py)
|
|
102
|
+
# ------------------------------------------------------------------
|
|
103
|
+
|
|
104
|
+
# MCP 工具名 → (wlkj 命令, 基础参数) 映射
|
|
105
|
+
# 只映射有 wlkj.py 对应的知识图谱/任务工具。
|
|
106
|
+
# MySQL/禅道/蓝湖等 MCP 专属工具不映射(MCP 不可用时返回错误提示)。
|
|
107
|
+
_TOOL_TO_WLKJ = {
|
|
108
|
+
"search_code": ("kg", ["search"]),
|
|
109
|
+
"search_api": ("kg", ["api"]),
|
|
110
|
+
"search_prd": ("kg", ["prd"]),
|
|
111
|
+
"context_pack": ("kg", ["context"]),
|
|
112
|
+
"get_impact": ("kg", ["impact"]),
|
|
113
|
+
"context_360": ("kg", ["context360"]),
|
|
114
|
+
"coverage_matrix": ("kg", ["coverage"]),
|
|
115
|
+
"feature_overview": ("kg", ["feature"]),
|
|
116
|
+
"get_workflow": ("kg", ["workflow"]),
|
|
117
|
+
"multi_hop": ("kg", ["hop"]),
|
|
118
|
+
"search_wiki": ("kg", ["wiki"]),
|
|
119
|
+
"fill_prototype": ("kg", ["fill-prototype"]),
|
|
120
|
+
"get_design_system": ("kg", ["design-system"]),
|
|
121
|
+
# RAG/GraphRAG 降级映射(MCP 挂时走本地 wlkj.py kg search,语义降级为关键词)
|
|
122
|
+
"rag_search": ("kg", ["search"]),
|
|
123
|
+
"ask_corpus": ("kg", ["search"]),
|
|
124
|
+
# 学习工具降级(MCP 挂时本地无 learning 表,返回提示)
|
|
125
|
+
"learn": ("noop", []),
|
|
126
|
+
"get_learnings": ("noop", []),
|
|
127
|
+
"list_tasks": ("task", ["list"]),
|
|
128
|
+
"create_task": ("task", ["create"]),
|
|
129
|
+
"start_task": ("task", ["start"]),
|
|
130
|
+
"finish_task": ("task", ["finish"]),
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
def _cli_fallback(self, tool_name: str, args: dict) -> Optional[str]:
|
|
134
|
+
"""MCP 工具名 → wlkj.py 执行。无映射返回 None。"""
|
|
135
|
+
entry = self._TOOL_TO_WLKJ.get(tool_name)
|
|
136
|
+
if entry is None:
|
|
137
|
+
return None
|
|
138
|
+
cmd, base_args = entry
|
|
139
|
+
# noop 类工具(learn/get_learnings 等):本地无等价 CLI,返回提示不崩
|
|
140
|
+
if cmd == "noop":
|
|
141
|
+
return json.dumps({"degraded": True, "message": "MCP不可用,该工具(learning)降级跳过"}, ensure_ascii=False)
|
|
142
|
+
cli_args = list(base_args)
|
|
143
|
+
# 从 MCP args 构建命令行参数。
|
|
144
|
+
# 参数名对齐: 平台 MCP search 工具入参是 query, 引擎命令文档用 keyword。
|
|
145
|
+
# 这里两边都取值, 确保无论上游传哪个都能落到 CLI 查询词上。
|
|
146
|
+
kw = args.get("keyword") or args.get("query")
|
|
147
|
+
if kw is not None:
|
|
148
|
+
cli_args.append(str(kw))
|
|
149
|
+
if "platform" in args and args["platform"]:
|
|
150
|
+
cli_args.extend(["--platform", str(args["platform"])])
|
|
151
|
+
if "role" in args and args["role"]:
|
|
152
|
+
cli_args.extend(["--role", str(args["role"])])
|
|
153
|
+
if "endpoint" in args:
|
|
154
|
+
cli_args.append(str(args["endpoint"]))
|
|
155
|
+
if "symbol" in args:
|
|
156
|
+
cli_args.append(str(args["symbol"]))
|
|
157
|
+
if "depth" in args:
|
|
158
|
+
cli_args.extend(["--depth", str(args["depth"])])
|
|
159
|
+
if "feature" in args:
|
|
160
|
+
cli_args.append(str(args["feature"]))
|
|
161
|
+
if "module" in args:
|
|
162
|
+
cli_args.append(str(args["module"]))
|
|
163
|
+
if "title" in args:
|
|
164
|
+
cli_args.append(str(args["title"]))
|
|
165
|
+
if "table" in args:
|
|
166
|
+
cli_args.append(str(args["table"]))
|
|
167
|
+
if "column" in args:
|
|
168
|
+
cli_args.append(str(args["column"]))
|
|
169
|
+
if "db" in args and args["db"]:
|
|
170
|
+
cli_args.extend(["--db", str(args["db"])])
|
|
171
|
+
if "env" in args and args["env"]:
|
|
172
|
+
cli_args.extend(["--env", str(args["env"])])
|
|
173
|
+
return self._run_wlkj(cmd, cli_args)
|
|
174
|
+
|
|
175
|
+
@staticmethod
|
|
176
|
+
def _run_wlkj(cmd: str, args: list) -> str:
|
|
177
|
+
"""调 wlkj.py 执行,返回 stdout。"""
|
|
178
|
+
import subprocess, sys, os
|
|
179
|
+
try:
|
|
180
|
+
from foundation.core.paths import get_repo_root
|
|
181
|
+
repo = str(get_repo_root())
|
|
182
|
+
except Exception:
|
|
183
|
+
repo = os.getcwd()
|
|
184
|
+
wlkj = os.path.join(repo, ".qoder", "scripts", "orchestration", "wlkj.py")
|
|
185
|
+
try:
|
|
186
|
+
r = subprocess.run(
|
|
187
|
+
[sys.executable, wlkj, cmd] + args,
|
|
188
|
+
capture_output=True, text=True, timeout=60,
|
|
189
|
+
encoding="utf-8", errors="replace",
|
|
190
|
+
)
|
|
191
|
+
if r.returncode == 0:
|
|
192
|
+
return r.stdout.strip()
|
|
193
|
+
return "[cli-fallback error] %s" % (r.stderr or r.stdout)[:200]
|
|
194
|
+
except subprocess.TimeoutExpired:
|
|
195
|
+
return "[cli-fallback error] timeout (60s)"
|
|
196
|
+
except Exception as e:
|
|
197
|
+
return "[cli-fallback error] %s" % str(e)[:100]
|
|
198
|
+
|
|
199
|
+
def list_tools(self, keyword: str = "") -> list[dict]:
|
|
200
|
+
"""枚举可用 MCP 工具(可选按 keyword 过滤)。"""
|
|
201
|
+
try:
|
|
202
|
+
self._registry._ensure_index()
|
|
203
|
+
tools = []
|
|
204
|
+
for server_name in self._registry._servers:
|
|
205
|
+
t = self._registry._pool.get(server_name)
|
|
206
|
+
if t is None:
|
|
207
|
+
continue
|
|
208
|
+
for tool in (t._tools_cache or []):
|
|
209
|
+
if keyword and keyword not in tool.get("name", ""):
|
|
210
|
+
continue
|
|
211
|
+
tool_copy = dict(tool)
|
|
212
|
+
tool_copy["_server"] = server_name
|
|
213
|
+
tools.append(tool_copy)
|
|
214
|
+
return tools
|
|
215
|
+
except Exception:
|
|
216
|
+
return []
|
|
217
|
+
|
|
218
|
+
def describe(self) -> dict:
|
|
219
|
+
"""诊断:各 server 状态。"""
|
|
220
|
+
try:
|
|
221
|
+
return self._registry.describe()
|
|
222
|
+
except Exception as e:
|
|
223
|
+
return {"error": str(e)[:120]}
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
class McpMemoryProvider:
|
|
227
|
+
"""memory 职责链的 MCP provider —— 接通平台 PG.learning_patterns。
|
|
228
|
+
|
|
229
|
+
设计模式: Adapter(把平台 MCP get_learnings/learn 适配成 MemoryCap 契约)
|
|
230
|
+
|
|
231
|
+
职责链位置: 链头优先。平台 MCP 可用(has_tool get_learnings)时抢占,
|
|
232
|
+
复用项目级学习表(PG.learning_patterns,按 token 解析 pid 隔离,跨会话积累)。
|
|
233
|
+
|
|
234
|
+
数据源: 平台 MCP 工具 get_learnings / learn(后端 services.knowledge.learn)。
|
|
235
|
+
"""
|
|
236
|
+
|
|
237
|
+
def __init__(self):
|
|
238
|
+
self._registry = None # 懒取 MCP 单例(避免 import 时即连后端)
|
|
239
|
+
|
|
240
|
+
def _reg(self):
|
|
241
|
+
"""复用 McpCap 同款模块级单例(_get_registry),零重复 spawn。"""
|
|
242
|
+
if self._registry is None:
|
|
243
|
+
self._registry = _get_registry()
|
|
244
|
+
return self._registry
|
|
245
|
+
|
|
246
|
+
@property
|
|
247
|
+
def available(self) -> bool:
|
|
248
|
+
"""平台 MCP 可用且 get_learnings 工具在索引中即为可用。
|
|
249
|
+
全迁 PG 后记忆走平台 get_learnings(PG.learning_patterns,按项目隔离)。
|
|
250
|
+
"""
|
|
251
|
+
try:
|
|
252
|
+
return self._reg().has_tool("get_learnings")
|
|
253
|
+
except Exception:
|
|
254
|
+
return False
|
|
255
|
+
|
|
256
|
+
def recall(self, query: str) -> list[dict]:
|
|
257
|
+
"""检索记忆: 取平台学习记录,按 query 模糊过滤 pattern/category。"""
|
|
258
|
+
if not self.available:
|
|
259
|
+
return []
|
|
260
|
+
try:
|
|
261
|
+
import json as _json
|
|
262
|
+
result = self._reg().call("get_learnings", {"limit": 50})
|
|
263
|
+
text = getattr(result, "text", "") or ""
|
|
264
|
+
data = _json.loads(text) if text else {}
|
|
265
|
+
items = data.get("items", []) if isinstance(data, dict) else []
|
|
266
|
+
q = (query or "").lower()
|
|
267
|
+
out = []
|
|
268
|
+
for it in items:
|
|
269
|
+
hay = "%s %s" % (it.get("category", ""), it.get("pattern", ""))
|
|
270
|
+
if not q or q in hay.lower():
|
|
271
|
+
out.append({
|
|
272
|
+
"event": it.get("category", ""),
|
|
273
|
+
"data": it.get("pattern", ""),
|
|
274
|
+
"ts": it.get("last_seen", ""),
|
|
275
|
+
"dev": ",".join(it.get("source_devs", []) or []),
|
|
276
|
+
})
|
|
277
|
+
return out[:20]
|
|
278
|
+
except Exception:
|
|
279
|
+
return []
|
|
280
|
+
|
|
281
|
+
def remember(self, key: str, content: str) -> None:
|
|
282
|
+
"""写入一条记忆: 调平台 learn(PG.learning_patterns 累加)。"""
|
|
283
|
+
if not self.available:
|
|
284
|
+
return
|
|
285
|
+
try:
|
|
286
|
+
self._reg().call("learn", {
|
|
287
|
+
"category": "rule",
|
|
288
|
+
"pattern": "%s: %s" % (key, content[:500]),
|
|
289
|
+
"source": "engine/memory",
|
|
290
|
+
})
|
|
291
|
+
except Exception:
|
|
292
|
+
pass # 写入失败不阻塞
|
|
293
|
+
|
|
294
|
+
def get_fact(self, key: str) -> str | None:
|
|
295
|
+
"""按 key 取一条事实,无则 None。"""
|
|
296
|
+
results = self.recall(key)
|
|
297
|
+
return results[0].get("data") if results else None
|
|
298
|
+
|
|
299
|
+
def list_recent(self, limit: int = 10) -> list[dict]:
|
|
300
|
+
"""最近 N 条记忆(平台按 frequency 倒序返回)。"""
|
|
301
|
+
if not self.available:
|
|
302
|
+
return []
|
|
303
|
+
try:
|
|
304
|
+
import json as _json
|
|
305
|
+
result = self._reg().call("get_learnings", {"limit": limit})
|
|
306
|
+
text = getattr(result, "text", "") or ""
|
|
307
|
+
data = _json.loads(text) if text else {}
|
|
308
|
+
items = data.get("items", []) if isinstance(data, dict) else []
|
|
309
|
+
return [{"event": it.get("category", ""), "data": it.get("pattern", ""),
|
|
310
|
+
"ts": it.get("last_seen", ""), "dev": ",".join(it.get("source_devs", []) or [])}
|
|
311
|
+
for it in items[:limit]]
|
|
312
|
+
except Exception:
|
|
313
|
+
return []
|
|
314
|
+
|