@hupan56/wlkj 3.4.9 → 3.5.1
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 +1713 -1713
- package/package.json +1 -1
- package/templates/qoder/config.yaml +229 -229
- package/templates/qoder/scripts/deployment/setup/install_qoderwork.py +835 -835
- package/templates/qoder/scripts/protocol/mcp/kg_mcp_server.py +351 -351
|
@@ -1,351 +1,351 @@
|
|
|
1
|
-
#!/usr/bin/env python
|
|
2
|
-
# -*- coding: utf-8 -*-
|
|
3
|
-
# v3.0 路径自举: 引导到 common/bootstrap, 统一 sys.path 逻辑
|
|
4
|
-
import os as _o, sys as _s
|
|
5
|
-
_f = _o.path.abspath(__file__)
|
|
6
|
-
for _ in range(10):
|
|
7
|
-
_f = _o.path.dirname(_f)
|
|
8
|
-
_cp = _o.path.join(_f, 'foundation')
|
|
9
|
-
if _o.path.isfile(_o.path.join(_cp, 'bootstrap.py')):
|
|
10
|
-
break
|
|
11
|
-
if _cp not in _s.path: _s.path.insert(0, _cp)
|
|
12
|
-
from bootstrap import setup; setup()
|
|
13
|
-
from foundation.core.paths import get_repo_root
|
|
14
|
-
|
|
15
|
-
"""
|
|
16
|
-
Knowledge Graph MCP Server for QoderWork (pure Python, no SDK dependency).
|
|
17
|
-
|
|
18
|
-
Exposes the QODER knowledge graph as MCP tools that QoderWork's AI agent
|
|
19
|
-
can call directly. Uses the MCP stdio protocol (JSON-RPC 2.0 over stdin/stdout)
|
|
20
|
-
implemented from scratch — no external dependencies beyond the standard library.
|
|
21
|
-
|
|
22
|
-
Registered in ~/.qoderwork/mcp.json as "qoder-knowledge-graph".
|
|
23
|
-
Tools surface as mcp__qoder-knowledge-graph__<tool> to the AI.
|
|
24
|
-
|
|
25
|
-
v3.1 重构: 用 @tool 装饰器 + ToolRegistryMeta 元类, 消除:
|
|
26
|
-
- 手写 TOOLS 列表 (78-253 行)
|
|
27
|
-
- _execute_tool 的 17 个 if/elif 分支 (386-576 行)
|
|
28
|
-
两处合一, 新增工具只需加一个 @tool 方法。
|
|
29
|
-
"""
|
|
30
|
-
import io
|
|
31
|
-
import os
|
|
32
|
-
import sys
|
|
33
|
-
import json
|
|
34
|
-
import subprocess
|
|
35
|
-
|
|
36
|
-
# Ensure UTF-8 output
|
|
37
|
-
try:
|
|
38
|
-
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
|
|
39
|
-
sys.stderr.reconfigure(encoding='utf-8', errors='replace')
|
|
40
|
-
except Exception:
|
|
41
|
-
pass
|
|
42
|
-
|
|
43
|
-
# Locate the scripts dir
|
|
44
|
-
SCRIPTS_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # scripts/ 根
|
|
45
|
-
BASE_DIR = os.path.dirname(os.path.dirname(SCRIPTS_DIR)) # repo 根
|
|
46
|
-
sys.path.insert(0, SCRIPTS_DIR)
|
|
47
|
-
_KG_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
48
|
-
if _KG_DIR not in sys.path: sys.path.insert(0, _KG_DIR)
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
# ── stdout 捕获辅助 ───────────────────────────────────────────────────
|
|
52
|
-
# 底层脚本 (search_index/context_pack) 用 print 输出, MCP 需要捕获为字符串返回。
|
|
53
|
-
|
|
54
|
-
def _capture(func, *args, **kwargs):
|
|
55
|
-
"""Run a function, capture its stdout.
|
|
56
|
-
|
|
57
|
-
v3.0: sys.stdout 在 run() 后已是 stderr (基类 _setup_streams 重定向),
|
|
58
|
-
这里捕获时临时换回 buf, finally 还原。
|
|
59
|
-
"""
|
|
60
|
-
old = sys.stdout
|
|
61
|
-
buf = io.StringIO()
|
|
62
|
-
sys.stdout = buf
|
|
63
|
-
try:
|
|
64
|
-
func(*args, **kwargs)
|
|
65
|
-
except Exception as e:
|
|
66
|
-
buf.write('\n[error] %s' % str(e))
|
|
67
|
-
finally:
|
|
68
|
-
sys.stdout = old
|
|
69
|
-
return buf.getvalue().strip()
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
# Lazy imports (首次调用工具时才 import, 加快启动)
|
|
73
|
-
_search = None
|
|
74
|
-
_context = None
|
|
75
|
-
|
|
76
|
-
def _ensure_imports():
|
|
77
|
-
global _search, _context
|
|
78
|
-
if _search is None:
|
|
79
|
-
from domain.kg.search import search_index as _search
|
|
80
|
-
if _context is None:
|
|
81
|
-
from domain.kg.search import context_pack as _context
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
# ── MCP Server (继承 BaseMCPServer, @tool 装饰器自动分发) ──────────────
|
|
85
|
-
|
|
86
|
-
from foundation.protocol.mcp_base import BaseMCPServer, tool
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
def _require_kgdb():
|
|
90
|
-
"""检查 kg.duckdb 是否存在。存在返回 None; 不存在返回友好提示字符串。
|
|
91
|
-
|
|
92
|
-
★ 共识:本地引擎全走平台。kg MCP 走云 SSE url,不依赖本地 kg.duckdb。
|
|
93
|
-
但本文件是"本地 launcher 模式"的 server——只在 mcp.json 没配 SSE url 时被调用。
|
|
94
|
-
配了 SSE url 时 QoderWork 直连云平台,本文件根本不会启动。
|
|
95
|
-
所以这里只需检查本地 duckdb,报错了也只影响 launcher 模式(非共识路径)。
|
|
96
|
-
"""
|
|
97
|
-
try:
|
|
98
|
-
from foundation.core.paths import DATA_INDEX_DIR
|
|
99
|
-
db_path = DATA_INDEX_DIR / 'kg.duckdb'
|
|
100
|
-
if not db_path.is_file():
|
|
101
|
-
return ("知识图谱走云平台(mcp.json kg → SSE url),本地 kg.duckdb 不需要。\n"
|
|
102
|
-
"如果云平台不通,可本地构建: python .qoder/scripts/orchestration/wlkj.py kg-build")
|
|
103
|
-
except Exception:
|
|
104
|
-
pass
|
|
105
|
-
return None
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
class KnowledgeGraphMCPServer(BaseMCPServer):
|
|
109
|
-
"""知识图谱 MCP Server。
|
|
110
|
-
|
|
111
|
-
v3.1: 每个工具用 @tool 装饰, 元类自动收集 TOOLS + 自动 handle_tool 分发。
|
|
112
|
-
新增工具 = 加一个 @tool 方法, 无需改 TOOLS 列表或 if 链。
|
|
113
|
-
"""
|
|
114
|
-
|
|
115
|
-
SERVER_NAME = "qoder-knowledge-graph"
|
|
116
|
-
SERVER_VERSION = "2.0.0"
|
|
117
|
-
|
|
118
|
-
# ★ v2.0 共识改造:kg 走云平台。本 server 的所有工具不再直连本地 duckdb,
|
|
119
|
-
# 而是转发到云平台 HTTP MCP@10010(http://10.
|
|
120
|
-
# 工具名映射:本地工具名 → 云平台等价 MCP 工具
|
|
121
|
-
PLATFORM_MCP_URL = os.environ.get("WLKJ_PLATFORM_MCP", "http://10.
|
|
122
|
-
# 本地工具 → 云平台工具映射(名称不同或参数不同)
|
|
123
|
-
_TOOL_MAP = {
|
|
124
|
-
"search_code": "search_code",
|
|
125
|
-
"search_api": "search_api",
|
|
126
|
-
"search_prd": "search_prd_semantic",
|
|
127
|
-
"get_impact": "get_impact",
|
|
128
|
-
"context_360": "context_360",
|
|
129
|
-
"context_pack": "context_pack",
|
|
130
|
-
"get_workflow": "business_trace",
|
|
131
|
-
"multi_hop": "business_trace",
|
|
132
|
-
"search_wiki": "rag_search",
|
|
133
|
-
"search_field": "search_field",
|
|
134
|
-
"list_modules": "get_modules",
|
|
135
|
-
"coverage_matrix": "impact_coverage",
|
|
136
|
-
"feature_overview": "feature_profile",
|
|
137
|
-
"table_impact": "table_impact",
|
|
138
|
-
"fill_prototype": None,
|
|
139
|
-
"get_design_system": "get_design_system",
|
|
140
|
-
"semantic_search": "rag_search",
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
def _call_platform(self, tool_name, args):
|
|
144
|
-
"""转发工具调用到云平台。
|
|
145
|
-
|
|
146
|
-
主路径:mcp.json kg→SSE url,QoderWork直连云平台(不经本server)。
|
|
147
|
-
备选路径:launcher模式(本server被调),走SSE协议转发。
|
|
148
|
-
"""
|
|
149
|
-
mapped = self._TOOL_MAP.get(tool_name)
|
|
150
|
-
if not mapped:
|
|
151
|
-
return json.dumps({"error": f"工具 {tool_name} 无云平台等价(本地功能)"}, ensure_ascii=False)
|
|
152
|
-
# 尝试HTTP POST(简单协议,部分端点支持)
|
|
153
|
-
try:
|
|
154
|
-
import urllib.request, urllib.error
|
|
155
|
-
body = json.dumps({"tool": mapped, "args": args or {}}).encode("utf-8")
|
|
156
|
-
req = urllib.request.Request(
|
|
157
|
-
self.PLATFORM_MCP_URL,
|
|
158
|
-
data=body,
|
|
159
|
-
headers={"Content-Type": "application/json"},
|
|
160
|
-
method="POST",
|
|
161
|
-
)
|
|
162
|
-
with urllib.request.urlopen(req, timeout=30) as resp:
|
|
163
|
-
return resp.read().decode("utf-8")
|
|
164
|
-
except urllib.error.HTTPError as e:
|
|
165
|
-
if e.code == 401:
|
|
166
|
-
return json.dumps({"error": "云平台需鉴权。请在 mcp.json 配 kg 为 SSE url(npx @hupan56/wlkj update 自动配置)。"}, ensure_ascii=False)
|
|
167
|
-
return json.dumps({"error": f"云平台 HTTP {e.code}: {e.reason}"}, ensure_ascii=False)
|
|
168
|
-
except Exception as e:
|
|
169
|
-
return json.dumps({"error": f"云平台不可达: {type(e).__name__}: {str(e)[:120]}"}, ensure_ascii=False)
|
|
170
|
-
|
|
171
|
-
# ── 工具实现(全部转发到云平台)──────────────────────────────────
|
|
172
|
-
|
|
173
|
-
@tool("search_code",
|
|
174
|
-
"搜索代码: 按关键词查找代码文件在哪。例: search_code(keyword='考勤') 或 search_code(keyword='车辆', platform='web')",
|
|
175
|
-
{"type": "object", "properties": {
|
|
176
|
-
"keyword": {"type": "string", "description": "搜索关键词(中文或英文)"},
|
|
177
|
-
"platform": {"type": "string", "enum": ["web", "app"], "description": "平台: web=PC管理端, app=移动端。可选"},
|
|
178
|
-
}, "required": ["keyword"]})
|
|
179
|
-
def _t_search_code(self, args):
|
|
180
|
-
return self._call_platform("search_code", args)
|
|
181
|
-
|
|
182
|
-
@tool("search_code",
|
|
183
|
-
"搜索代码: 按关键词查找代码文件在哪。例: search_code(keyword='考勤') 或 search_code(keyword='车辆', platform='web')",
|
|
184
|
-
{"type": "object", "properties": {
|
|
185
|
-
"keyword": {"type": "string", "description": "搜索关键词(中文或英文)"},
|
|
186
|
-
"platform": {"type": "string", "enum": ["web", "app"], "description": "平台: web=PC管理端, app=移动端。可选"},
|
|
187
|
-
}, "required": ["keyword"]})
|
|
188
|
-
def _t_search_code(self, args):
|
|
189
|
-
return self._call_platform("search_code", args)
|
|
190
|
-
|
|
191
|
-
@tool("search_api",
|
|
192
|
-
"搜索API端点: 按关键词查找后端接口。例: search_api(keyword='salary')",
|
|
193
|
-
{"type": "object", "properties": {
|
|
194
|
-
"keyword": {"type": "string", "description": "API关键词"},
|
|
195
|
-
}, "required": ["keyword"]})
|
|
196
|
-
def _t_search_api(self, args):
|
|
197
|
-
return self._call_platform("search_api", args)
|
|
198
|
-
|
|
199
|
-
@tool("search_prd",
|
|
200
|
-
"搜索PRD: 查已有需求文档,防止重复造轮子。例: search_prd(keyword='考勤')",
|
|
201
|
-
{"type": "object", "properties": {
|
|
202
|
-
"keyword": {"type": "string", "description": "PRD关键词"},
|
|
203
|
-
}, "required": ["keyword"]})
|
|
204
|
-
def _t_search_prd(self, args):
|
|
205
|
-
return self._call_platform("search_prd", args)
|
|
206
|
-
|
|
207
|
-
@tool("get_impact",
|
|
208
|
-
"影响分析: 改某接口/路径会影响哪些前端页面和按钮?评估改动风险。例: get_impact(endpoint='/asset') 或 get_impact(endpoint='export')",
|
|
209
|
-
{"type": "object", "properties": {
|
|
210
|
-
"endpoint": {"type": "string", "description": "API端点路径或关键词, 如 /asset/list 或 export"},
|
|
211
|
-
}, "required": ["endpoint"]})
|
|
212
|
-
def _t_get_impact(self, args):
|
|
213
|
-
return self._call_platform("get_impact", args)
|
|
214
|
-
|
|
215
|
-
@tool("context_360",
|
|
216
|
-
"360度视图: 查看符号(端点/函数/处理器)的全部关联信息。例: context_360(symbol='handleExport') 或 context_360(symbol='/system/role')",
|
|
217
|
-
{"type": "object", "properties": {
|
|
218
|
-
"symbol": {"type": "string", "description": "符号名: 端点/函数名/处理器名"},
|
|
219
|
-
}, "required": ["symbol"]})
|
|
220
|
-
def _t_context_360(self, args):
|
|
221
|
-
return self._call_platform("context_360", args)
|
|
222
|
-
|
|
223
|
-
@tool("context_pack",
|
|
224
|
-
"上下文打包: 一次返回关键词相关的全部上下文(代码+页面+字段+PRD+API+布局指纹)。避免来回查。例: context_pack(keyword='车辆', platform='web')",
|
|
225
|
-
{"type": "object", "properties": {
|
|
226
|
-
"keyword": {"type": "string", "description": "业务关键词"},
|
|
227
|
-
"platform": {"type": "string", "enum": ["web", "app"], "description": "平台: web或app。可选"},
|
|
228
|
-
"page_type": {"type": "string", "description": "页面类型: table-page/form-page/detail-page。可选"},
|
|
229
|
-
"role": {"type": "string", "enum": ["pm", "design", "dev", "test", "admin"],
|
|
230
|
-
"description": "角色裁剪: pm=字段+PRD+API, design=页面+布局+风格, dev=代码+API, test=代码+PRD+API。可选"},
|
|
231
|
-
}, "required": ["keyword"]})
|
|
232
|
-
def _t_context_pack(self, args):
|
|
233
|
-
return self._call_platform("context_pack", args)
|
|
234
|
-
|
|
235
|
-
@tool("get_workflow",
|
|
236
|
-
"业务流程: 查看一个模块的完整操作链。",
|
|
237
|
-
{"type": "object", "properties": {
|
|
238
|
-
"module": {"type": "string", "description": "模块名(英文优先)"},
|
|
239
|
-
}, "required": ["module"]})
|
|
240
|
-
def _t_get_workflow(self, args):
|
|
241
|
-
return self._call_platform("get_workflow", args)
|
|
242
|
-
|
|
243
|
-
@tool("multi_hop",
|
|
244
|
-
"知识图谱多跳遍历: 从一个符号出发, 沿edges递归N跳。",
|
|
245
|
-
{"type": "object", "properties": {
|
|
246
|
-
"symbol": {"type": "string", "description": "起点符号"},
|
|
247
|
-
"max_depth": {"type": "integer", "default": 3, "description": "最大跳数(1-5)"},
|
|
248
|
-
}, "required": ["symbol"]})
|
|
249
|
-
def _t_multi_hop(self, args):
|
|
250
|
-
return self._call_platform("multi_hop", args)
|
|
251
|
-
|
|
252
|
-
@tool("search_wiki",
|
|
253
|
-
"搜索Repo Wiki: 查模块级语义文档。",
|
|
254
|
-
{"type": "object", "properties": {
|
|
255
|
-
"keyword": {"type": "string", "description": "Wiki关键词"},
|
|
256
|
-
}, "required": ["keyword"]})
|
|
257
|
-
def _t_search_wiki(self, args):
|
|
258
|
-
return self._call_platform("search_wiki", args)
|
|
259
|
-
|
|
260
|
-
@tool("search_field",
|
|
261
|
-
"搜索字段: 查某个字段在哪些页面用过。",
|
|
262
|
-
{"type": "object", "properties": {
|
|
263
|
-
"field": {"type": "string", "description": "字段名(英文field或中文标题)"},
|
|
264
|
-
}, "required": ["field"]})
|
|
265
|
-
def _t_search_field(self, args):
|
|
266
|
-
return self._call_platform("search_field", args)
|
|
267
|
-
|
|
268
|
-
@tool("list_modules",
|
|
269
|
-
"模块清单: 列出项目所有业务模块。",
|
|
270
|
-
{"type": "object", "properties": {
|
|
271
|
-
"platform": {"type": "string", "enum": ["web", "app"], "description": "平台过滤"},
|
|
272
|
-
}})
|
|
273
|
-
def _t_list_modules(self, args):
|
|
274
|
-
return self._call_platform("list_modules", args)
|
|
275
|
-
|
|
276
|
-
@tool("coverage_matrix",
|
|
277
|
-
"功能×测试覆盖矩阵。",
|
|
278
|
-
{"type": "object", "properties": {}})
|
|
279
|
-
def _t_coverage_matrix(self, args):
|
|
280
|
-
return self._call_platform("coverage_matrix", args)
|
|
281
|
-
|
|
282
|
-
@tool("feature_overview",
|
|
283
|
-
"功能画像: 一个功能的完整画像。",
|
|
284
|
-
{"type": "object", "properties": {
|
|
285
|
-
"feature": {"type": "string", "description": "功能名(中/英文)"},
|
|
286
|
-
}, "required": ["feature"]})
|
|
287
|
-
def _t_feature_overview(self, args):
|
|
288
|
-
return self._call_platform("feature_overview", args)
|
|
289
|
-
|
|
290
|
-
@tool("table_impact",
|
|
291
|
-
"改表影响分析: 改某数据库表会影响哪些 Entity 类和接口。",
|
|
292
|
-
{"type": "object", "properties": {
|
|
293
|
-
"table": {"type": "string", "description": "数据库表名"},
|
|
294
|
-
}, "required": ["table"]})
|
|
295
|
-
def _t_table_impact(self, args):
|
|
296
|
-
return self._call_platform("table_impact", args)
|
|
297
|
-
|
|
298
|
-
# ── 原型/设计/语义 (全部走云平台) ────────────────────────────────
|
|
299
|
-
|
|
300
|
-
@tool("fill_prototype",
|
|
301
|
-
"原型预填: 用真实代码数据自动填模板。",
|
|
302
|
-
{"type": "object", "properties": {
|
|
303
|
-
"keyword": {"type": "string", "description": "业务关键词"},
|
|
304
|
-
"platform": {"type": "string", "enum": ["web", "app"], "description": "平台"},
|
|
305
|
-
}, "required": ["keyword"]})
|
|
306
|
-
def _t_fill_prototype(self, args):
|
|
307
|
-
return self._call_platform("fill_prototype", args)
|
|
308
|
-
|
|
309
|
-
@tool("get_design_system",
|
|
310
|
-
"设计系统查询: 返回项目完整设计规范。",
|
|
311
|
-
{"type": "object", "properties": {
|
|
312
|
-
"platform": {"type": "string", "enum": ["web", "app"], "description": "平台过滤"},
|
|
313
|
-
}})
|
|
314
|
-
def _t_get_design_system(self, args):
|
|
315
|
-
return self._call_platform("get_design_system", args)
|
|
316
|
-
|
|
317
|
-
@tool("semantic_search",
|
|
318
|
-
"语义搜索: 用自然语言找代码/API/功能。",
|
|
319
|
-
{"type": "object", "properties": {
|
|
320
|
-
"query": {"type": "string", "description": "自然语言查询"},
|
|
321
|
-
"top_k": {"type": "integer", "default": 10, "description": "返回前N条"},
|
|
322
|
-
}, "required": ["query"]})
|
|
323
|
-
def _t_semantic_search(self, args):
|
|
324
|
-
return self._call_platform("semantic_search", args)
|
|
325
|
-
from domain.kg.graph.kg_semantic import semantic_search, embedding_status
|
|
326
|
-
query = (args.get("query") or '').strip()
|
|
327
|
-
if not query:
|
|
328
|
-
return "请提供查询关键词"
|
|
329
|
-
try:
|
|
330
|
-
top_k = int(args.get("top_k", 10))
|
|
331
|
-
except (TypeError, ValueError):
|
|
332
|
-
top_k = 10
|
|
333
|
-
results = semantic_search(query, top_k=top_k)
|
|
334
|
-
if results is None:
|
|
335
|
-
st = embedding_status()
|
|
336
|
-
if not st['available']:
|
|
337
|
-
return ("语义检索未启用: sentence-transformers 未安装。\n"
|
|
338
|
-
"启用方法: pip install sentence-transformers\n"
|
|
339
|
-
"(95MB 模型首次自动下载, 之后完全离线)")
|
|
340
|
-
elif st['embeddings_count'] == 0:
|
|
341
|
-
return ("语义检索未构建向量。请管理员运行:\n"
|
|
342
|
-
" python .qoder/scripts/domain/kg/graph/kg_semantic.py build-embeddings")
|
|
343
|
-
return None
|
|
344
|
-
lines = ["语义搜索: '%s' (top %d)" % (query, len(results))]
|
|
345
|
-
for eid, text, score in results:
|
|
346
|
-
lines.append(" %.3f %s" % (score, text[:70]))
|
|
347
|
-
return '\n'.join(lines)
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
if __name__ == "__main__":
|
|
351
|
-
KnowledgeGraphMCPServer().run()
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
# v3.0 路径自举: 引导到 common/bootstrap, 统一 sys.path 逻辑
|
|
4
|
+
import os as _o, sys as _s
|
|
5
|
+
_f = _o.path.abspath(__file__)
|
|
6
|
+
for _ in range(10):
|
|
7
|
+
_f = _o.path.dirname(_f)
|
|
8
|
+
_cp = _o.path.join(_f, 'foundation')
|
|
9
|
+
if _o.path.isfile(_o.path.join(_cp, 'bootstrap.py')):
|
|
10
|
+
break
|
|
11
|
+
if _cp not in _s.path: _s.path.insert(0, _cp)
|
|
12
|
+
from bootstrap import setup; setup()
|
|
13
|
+
from foundation.core.paths import get_repo_root
|
|
14
|
+
|
|
15
|
+
"""
|
|
16
|
+
Knowledge Graph MCP Server for QoderWork (pure Python, no SDK dependency).
|
|
17
|
+
|
|
18
|
+
Exposes the QODER knowledge graph as MCP tools that QoderWork's AI agent
|
|
19
|
+
can call directly. Uses the MCP stdio protocol (JSON-RPC 2.0 over stdin/stdout)
|
|
20
|
+
implemented from scratch — no external dependencies beyond the standard library.
|
|
21
|
+
|
|
22
|
+
Registered in ~/.qoderwork/mcp.json as "qoder-knowledge-graph".
|
|
23
|
+
Tools surface as mcp__qoder-knowledge-graph__<tool> to the AI.
|
|
24
|
+
|
|
25
|
+
v3.1 重构: 用 @tool 装饰器 + ToolRegistryMeta 元类, 消除:
|
|
26
|
+
- 手写 TOOLS 列表 (78-253 行)
|
|
27
|
+
- _execute_tool 的 17 个 if/elif 分支 (386-576 行)
|
|
28
|
+
两处合一, 新增工具只需加一个 @tool 方法。
|
|
29
|
+
"""
|
|
30
|
+
import io
|
|
31
|
+
import os
|
|
32
|
+
import sys
|
|
33
|
+
import json
|
|
34
|
+
import subprocess
|
|
35
|
+
|
|
36
|
+
# Ensure UTF-8 output
|
|
37
|
+
try:
|
|
38
|
+
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
|
|
39
|
+
sys.stderr.reconfigure(encoding='utf-8', errors='replace')
|
|
40
|
+
except Exception:
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
# Locate the scripts dir
|
|
44
|
+
SCRIPTS_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # scripts/ 根
|
|
45
|
+
BASE_DIR = os.path.dirname(os.path.dirname(SCRIPTS_DIR)) # repo 根
|
|
46
|
+
sys.path.insert(0, SCRIPTS_DIR)
|
|
47
|
+
_KG_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
48
|
+
if _KG_DIR not in sys.path: sys.path.insert(0, _KG_DIR)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# ── stdout 捕获辅助 ───────────────────────────────────────────────────
|
|
52
|
+
# 底层脚本 (search_index/context_pack) 用 print 输出, MCP 需要捕获为字符串返回。
|
|
53
|
+
|
|
54
|
+
def _capture(func, *args, **kwargs):
|
|
55
|
+
"""Run a function, capture its stdout.
|
|
56
|
+
|
|
57
|
+
v3.0: sys.stdout 在 run() 后已是 stderr (基类 _setup_streams 重定向),
|
|
58
|
+
这里捕获时临时换回 buf, finally 还原。
|
|
59
|
+
"""
|
|
60
|
+
old = sys.stdout
|
|
61
|
+
buf = io.StringIO()
|
|
62
|
+
sys.stdout = buf
|
|
63
|
+
try:
|
|
64
|
+
func(*args, **kwargs)
|
|
65
|
+
except Exception as e:
|
|
66
|
+
buf.write('\n[error] %s' % str(e))
|
|
67
|
+
finally:
|
|
68
|
+
sys.stdout = old
|
|
69
|
+
return buf.getvalue().strip()
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
# Lazy imports (首次调用工具时才 import, 加快启动)
|
|
73
|
+
_search = None
|
|
74
|
+
_context = None
|
|
75
|
+
|
|
76
|
+
def _ensure_imports():
|
|
77
|
+
global _search, _context
|
|
78
|
+
if _search is None:
|
|
79
|
+
from domain.kg.search import search_index as _search
|
|
80
|
+
if _context is None:
|
|
81
|
+
from domain.kg.search import context_pack as _context
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
# ── MCP Server (继承 BaseMCPServer, @tool 装饰器自动分发) ──────────────
|
|
85
|
+
|
|
86
|
+
from foundation.protocol.mcp_base import BaseMCPServer, tool
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _require_kgdb():
|
|
90
|
+
"""检查 kg.duckdb 是否存在。存在返回 None; 不存在返回友好提示字符串。
|
|
91
|
+
|
|
92
|
+
★ 共识:本地引擎全走平台。kg MCP 走云 SSE url,不依赖本地 kg.duckdb。
|
|
93
|
+
但本文件是"本地 launcher 模式"的 server——只在 mcp.json 没配 SSE url 时被调用。
|
|
94
|
+
配了 SSE url 时 QoderWork 直连云平台,本文件根本不会启动。
|
|
95
|
+
所以这里只需检查本地 duckdb,报错了也只影响 launcher 模式(非共识路径)。
|
|
96
|
+
"""
|
|
97
|
+
try:
|
|
98
|
+
from foundation.core.paths import DATA_INDEX_DIR
|
|
99
|
+
db_path = DATA_INDEX_DIR / 'kg.duckdb'
|
|
100
|
+
if not db_path.is_file():
|
|
101
|
+
return ("知识图谱走云平台(mcp.json kg → SSE url),本地 kg.duckdb 不需要。\n"
|
|
102
|
+
"如果云平台不通,可本地构建: python .qoder/scripts/orchestration/wlkj.py kg-build")
|
|
103
|
+
except Exception:
|
|
104
|
+
pass
|
|
105
|
+
return None
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class KnowledgeGraphMCPServer(BaseMCPServer):
|
|
109
|
+
"""知识图谱 MCP Server。
|
|
110
|
+
|
|
111
|
+
v3.1: 每个工具用 @tool 装饰, 元类自动收集 TOOLS + 自动 handle_tool 分发。
|
|
112
|
+
新增工具 = 加一个 @tool 方法, 无需改 TOOLS 列表或 if 链。
|
|
113
|
+
"""
|
|
114
|
+
|
|
115
|
+
SERVER_NAME = "qoder-knowledge-graph"
|
|
116
|
+
SERVER_VERSION = "2.0.0"
|
|
117
|
+
|
|
118
|
+
# ★ v2.0 共识改造:kg 走云平台。本 server 的所有工具不再直连本地 duckdb,
|
|
119
|
+
# 而是转发到云平台 HTTP MCP@10010(http://10.87.106.252/mcp)。
|
|
120
|
+
# 工具名映射:本地工具名 → 云平台等价 MCP 工具
|
|
121
|
+
PLATFORM_MCP_URL = os.environ.get("WLKJ_PLATFORM_MCP", "http://10.87.106.252/mcp")
|
|
122
|
+
# 本地工具 → 云平台工具映射(名称不同或参数不同)
|
|
123
|
+
_TOOL_MAP = {
|
|
124
|
+
"search_code": "search_code",
|
|
125
|
+
"search_api": "search_api",
|
|
126
|
+
"search_prd": "search_prd_semantic",
|
|
127
|
+
"get_impact": "get_impact",
|
|
128
|
+
"context_360": "context_360",
|
|
129
|
+
"context_pack": "context_pack",
|
|
130
|
+
"get_workflow": "business_trace",
|
|
131
|
+
"multi_hop": "business_trace",
|
|
132
|
+
"search_wiki": "rag_search",
|
|
133
|
+
"search_field": "search_field",
|
|
134
|
+
"list_modules": "get_modules",
|
|
135
|
+
"coverage_matrix": "impact_coverage",
|
|
136
|
+
"feature_overview": "feature_profile",
|
|
137
|
+
"table_impact": "table_impact",
|
|
138
|
+
"fill_prototype": None,
|
|
139
|
+
"get_design_system": "get_design_system",
|
|
140
|
+
"semantic_search": "rag_search",
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
def _call_platform(self, tool_name, args):
|
|
144
|
+
"""转发工具调用到云平台。
|
|
145
|
+
|
|
146
|
+
主路径:mcp.json kg→SSE url,QoderWork直连云平台(不经本server)。
|
|
147
|
+
备选路径:launcher模式(本server被调),走SSE协议转发。
|
|
148
|
+
"""
|
|
149
|
+
mapped = self._TOOL_MAP.get(tool_name)
|
|
150
|
+
if not mapped:
|
|
151
|
+
return json.dumps({"error": f"工具 {tool_name} 无云平台等价(本地功能)"}, ensure_ascii=False)
|
|
152
|
+
# 尝试HTTP POST(简单协议,部分端点支持)
|
|
153
|
+
try:
|
|
154
|
+
import urllib.request, urllib.error
|
|
155
|
+
body = json.dumps({"tool": mapped, "args": args or {}}).encode("utf-8")
|
|
156
|
+
req = urllib.request.Request(
|
|
157
|
+
self.PLATFORM_MCP_URL,
|
|
158
|
+
data=body,
|
|
159
|
+
headers={"Content-Type": "application/json"},
|
|
160
|
+
method="POST",
|
|
161
|
+
)
|
|
162
|
+
with urllib.request.urlopen(req, timeout=30) as resp:
|
|
163
|
+
return resp.read().decode("utf-8")
|
|
164
|
+
except urllib.error.HTTPError as e:
|
|
165
|
+
if e.code == 401:
|
|
166
|
+
return json.dumps({"error": "云平台需鉴权。请在 mcp.json 配 kg 为 SSE url(npx @hupan56/wlkj update 自动配置)。"}, ensure_ascii=False)
|
|
167
|
+
return json.dumps({"error": f"云平台 HTTP {e.code}: {e.reason}"}, ensure_ascii=False)
|
|
168
|
+
except Exception as e:
|
|
169
|
+
return json.dumps({"error": f"云平台不可达: {type(e).__name__}: {str(e)[:120]}"}, ensure_ascii=False)
|
|
170
|
+
|
|
171
|
+
# ── 工具实现(全部转发到云平台)──────────────────────────────────
|
|
172
|
+
|
|
173
|
+
@tool("search_code",
|
|
174
|
+
"搜索代码: 按关键词查找代码文件在哪。例: search_code(keyword='考勤') 或 search_code(keyword='车辆', platform='web')",
|
|
175
|
+
{"type": "object", "properties": {
|
|
176
|
+
"keyword": {"type": "string", "description": "搜索关键词(中文或英文)"},
|
|
177
|
+
"platform": {"type": "string", "enum": ["web", "app"], "description": "平台: web=PC管理端, app=移动端。可选"},
|
|
178
|
+
}, "required": ["keyword"]})
|
|
179
|
+
def _t_search_code(self, args):
|
|
180
|
+
return self._call_platform("search_code", args)
|
|
181
|
+
|
|
182
|
+
@tool("search_code",
|
|
183
|
+
"搜索代码: 按关键词查找代码文件在哪。例: search_code(keyword='考勤') 或 search_code(keyword='车辆', platform='web')",
|
|
184
|
+
{"type": "object", "properties": {
|
|
185
|
+
"keyword": {"type": "string", "description": "搜索关键词(中文或英文)"},
|
|
186
|
+
"platform": {"type": "string", "enum": ["web", "app"], "description": "平台: web=PC管理端, app=移动端。可选"},
|
|
187
|
+
}, "required": ["keyword"]})
|
|
188
|
+
def _t_search_code(self, args):
|
|
189
|
+
return self._call_platform("search_code", args)
|
|
190
|
+
|
|
191
|
+
@tool("search_api",
|
|
192
|
+
"搜索API端点: 按关键词查找后端接口。例: search_api(keyword='salary')",
|
|
193
|
+
{"type": "object", "properties": {
|
|
194
|
+
"keyword": {"type": "string", "description": "API关键词"},
|
|
195
|
+
}, "required": ["keyword"]})
|
|
196
|
+
def _t_search_api(self, args):
|
|
197
|
+
return self._call_platform("search_api", args)
|
|
198
|
+
|
|
199
|
+
@tool("search_prd",
|
|
200
|
+
"搜索PRD: 查已有需求文档,防止重复造轮子。例: search_prd(keyword='考勤')",
|
|
201
|
+
{"type": "object", "properties": {
|
|
202
|
+
"keyword": {"type": "string", "description": "PRD关键词"},
|
|
203
|
+
}, "required": ["keyword"]})
|
|
204
|
+
def _t_search_prd(self, args):
|
|
205
|
+
return self._call_platform("search_prd", args)
|
|
206
|
+
|
|
207
|
+
@tool("get_impact",
|
|
208
|
+
"影响分析: 改某接口/路径会影响哪些前端页面和按钮?评估改动风险。例: get_impact(endpoint='/asset') 或 get_impact(endpoint='export')",
|
|
209
|
+
{"type": "object", "properties": {
|
|
210
|
+
"endpoint": {"type": "string", "description": "API端点路径或关键词, 如 /asset/list 或 export"},
|
|
211
|
+
}, "required": ["endpoint"]})
|
|
212
|
+
def _t_get_impact(self, args):
|
|
213
|
+
return self._call_platform("get_impact", args)
|
|
214
|
+
|
|
215
|
+
@tool("context_360",
|
|
216
|
+
"360度视图: 查看符号(端点/函数/处理器)的全部关联信息。例: context_360(symbol='handleExport') 或 context_360(symbol='/system/role')",
|
|
217
|
+
{"type": "object", "properties": {
|
|
218
|
+
"symbol": {"type": "string", "description": "符号名: 端点/函数名/处理器名"},
|
|
219
|
+
}, "required": ["symbol"]})
|
|
220
|
+
def _t_context_360(self, args):
|
|
221
|
+
return self._call_platform("context_360", args)
|
|
222
|
+
|
|
223
|
+
@tool("context_pack",
|
|
224
|
+
"上下文打包: 一次返回关键词相关的全部上下文(代码+页面+字段+PRD+API+布局指纹)。避免来回查。例: context_pack(keyword='车辆', platform='web')",
|
|
225
|
+
{"type": "object", "properties": {
|
|
226
|
+
"keyword": {"type": "string", "description": "业务关键词"},
|
|
227
|
+
"platform": {"type": "string", "enum": ["web", "app"], "description": "平台: web或app。可选"},
|
|
228
|
+
"page_type": {"type": "string", "description": "页面类型: table-page/form-page/detail-page。可选"},
|
|
229
|
+
"role": {"type": "string", "enum": ["pm", "design", "dev", "test", "admin"],
|
|
230
|
+
"description": "角色裁剪: pm=字段+PRD+API, design=页面+布局+风格, dev=代码+API, test=代码+PRD+API。可选"},
|
|
231
|
+
}, "required": ["keyword"]})
|
|
232
|
+
def _t_context_pack(self, args):
|
|
233
|
+
return self._call_platform("context_pack", args)
|
|
234
|
+
|
|
235
|
+
@tool("get_workflow",
|
|
236
|
+
"业务流程: 查看一个模块的完整操作链。",
|
|
237
|
+
{"type": "object", "properties": {
|
|
238
|
+
"module": {"type": "string", "description": "模块名(英文优先)"},
|
|
239
|
+
}, "required": ["module"]})
|
|
240
|
+
def _t_get_workflow(self, args):
|
|
241
|
+
return self._call_platform("get_workflow", args)
|
|
242
|
+
|
|
243
|
+
@tool("multi_hop",
|
|
244
|
+
"知识图谱多跳遍历: 从一个符号出发, 沿edges递归N跳。",
|
|
245
|
+
{"type": "object", "properties": {
|
|
246
|
+
"symbol": {"type": "string", "description": "起点符号"},
|
|
247
|
+
"max_depth": {"type": "integer", "default": 3, "description": "最大跳数(1-5)"},
|
|
248
|
+
}, "required": ["symbol"]})
|
|
249
|
+
def _t_multi_hop(self, args):
|
|
250
|
+
return self._call_platform("multi_hop", args)
|
|
251
|
+
|
|
252
|
+
@tool("search_wiki",
|
|
253
|
+
"搜索Repo Wiki: 查模块级语义文档。",
|
|
254
|
+
{"type": "object", "properties": {
|
|
255
|
+
"keyword": {"type": "string", "description": "Wiki关键词"},
|
|
256
|
+
}, "required": ["keyword"]})
|
|
257
|
+
def _t_search_wiki(self, args):
|
|
258
|
+
return self._call_platform("search_wiki", args)
|
|
259
|
+
|
|
260
|
+
@tool("search_field",
|
|
261
|
+
"搜索字段: 查某个字段在哪些页面用过。",
|
|
262
|
+
{"type": "object", "properties": {
|
|
263
|
+
"field": {"type": "string", "description": "字段名(英文field或中文标题)"},
|
|
264
|
+
}, "required": ["field"]})
|
|
265
|
+
def _t_search_field(self, args):
|
|
266
|
+
return self._call_platform("search_field", args)
|
|
267
|
+
|
|
268
|
+
@tool("list_modules",
|
|
269
|
+
"模块清单: 列出项目所有业务模块。",
|
|
270
|
+
{"type": "object", "properties": {
|
|
271
|
+
"platform": {"type": "string", "enum": ["web", "app"], "description": "平台过滤"},
|
|
272
|
+
}})
|
|
273
|
+
def _t_list_modules(self, args):
|
|
274
|
+
return self._call_platform("list_modules", args)
|
|
275
|
+
|
|
276
|
+
@tool("coverage_matrix",
|
|
277
|
+
"功能×测试覆盖矩阵。",
|
|
278
|
+
{"type": "object", "properties": {}})
|
|
279
|
+
def _t_coverage_matrix(self, args):
|
|
280
|
+
return self._call_platform("coverage_matrix", args)
|
|
281
|
+
|
|
282
|
+
@tool("feature_overview",
|
|
283
|
+
"功能画像: 一个功能的完整画像。",
|
|
284
|
+
{"type": "object", "properties": {
|
|
285
|
+
"feature": {"type": "string", "description": "功能名(中/英文)"},
|
|
286
|
+
}, "required": ["feature"]})
|
|
287
|
+
def _t_feature_overview(self, args):
|
|
288
|
+
return self._call_platform("feature_overview", args)
|
|
289
|
+
|
|
290
|
+
@tool("table_impact",
|
|
291
|
+
"改表影响分析: 改某数据库表会影响哪些 Entity 类和接口。",
|
|
292
|
+
{"type": "object", "properties": {
|
|
293
|
+
"table": {"type": "string", "description": "数据库表名"},
|
|
294
|
+
}, "required": ["table"]})
|
|
295
|
+
def _t_table_impact(self, args):
|
|
296
|
+
return self._call_platform("table_impact", args)
|
|
297
|
+
|
|
298
|
+
# ── 原型/设计/语义 (全部走云平台) ────────────────────────────────
|
|
299
|
+
|
|
300
|
+
@tool("fill_prototype",
|
|
301
|
+
"原型预填: 用真实代码数据自动填模板。",
|
|
302
|
+
{"type": "object", "properties": {
|
|
303
|
+
"keyword": {"type": "string", "description": "业务关键词"},
|
|
304
|
+
"platform": {"type": "string", "enum": ["web", "app"], "description": "平台"},
|
|
305
|
+
}, "required": ["keyword"]})
|
|
306
|
+
def _t_fill_prototype(self, args):
|
|
307
|
+
return self._call_platform("fill_prototype", args)
|
|
308
|
+
|
|
309
|
+
@tool("get_design_system",
|
|
310
|
+
"设计系统查询: 返回项目完整设计规范。",
|
|
311
|
+
{"type": "object", "properties": {
|
|
312
|
+
"platform": {"type": "string", "enum": ["web", "app"], "description": "平台过滤"},
|
|
313
|
+
}})
|
|
314
|
+
def _t_get_design_system(self, args):
|
|
315
|
+
return self._call_platform("get_design_system", args)
|
|
316
|
+
|
|
317
|
+
@tool("semantic_search",
|
|
318
|
+
"语义搜索: 用自然语言找代码/API/功能。",
|
|
319
|
+
{"type": "object", "properties": {
|
|
320
|
+
"query": {"type": "string", "description": "自然语言查询"},
|
|
321
|
+
"top_k": {"type": "integer", "default": 10, "description": "返回前N条"},
|
|
322
|
+
}, "required": ["query"]})
|
|
323
|
+
def _t_semantic_search(self, args):
|
|
324
|
+
return self._call_platform("semantic_search", args)
|
|
325
|
+
from domain.kg.graph.kg_semantic import semantic_search, embedding_status
|
|
326
|
+
query = (args.get("query") or '').strip()
|
|
327
|
+
if not query:
|
|
328
|
+
return "请提供查询关键词"
|
|
329
|
+
try:
|
|
330
|
+
top_k = int(args.get("top_k", 10))
|
|
331
|
+
except (TypeError, ValueError):
|
|
332
|
+
top_k = 10
|
|
333
|
+
results = semantic_search(query, top_k=top_k)
|
|
334
|
+
if results is None:
|
|
335
|
+
st = embedding_status()
|
|
336
|
+
if not st['available']:
|
|
337
|
+
return ("语义检索未启用: sentence-transformers 未安装。\n"
|
|
338
|
+
"启用方法: pip install sentence-transformers\n"
|
|
339
|
+
"(95MB 模型首次自动下载, 之后完全离线)")
|
|
340
|
+
elif st['embeddings_count'] == 0:
|
|
341
|
+
return ("语义检索未构建向量。请管理员运行:\n"
|
|
342
|
+
" python .qoder/scripts/domain/kg/graph/kg_semantic.py build-embeddings")
|
|
343
|
+
return None
|
|
344
|
+
lines = ["语义搜索: '%s' (top %d)" % (query, len(results))]
|
|
345
|
+
for eid, text, score in results:
|
|
346
|
+
lines.append(" %.3f %s" % (score, text[:70]))
|
|
347
|
+
return '\n'.join(lines)
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
if __name__ == "__main__":
|
|
351
|
+
KnowledgeGraphMCPServer().run()
|