@hupan56/wlkj 2.7.11 → 3.0.0
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 +375 -78
- package/package.json +29 -29
- package/templates/.qoder/.runtime/ctx-cache-5660152f1d6dd819.md +23 -0
- package/templates/.qoder/.runtime/ctx-cache-afdce0dac06b25b0.md +23 -0
- package/templates/.qoder/.runtime/search-cache-eae7644e7b122f35.txt +1 -0
- package/templates/.qoder/learning/eval-history.jsonl +28 -0
- package/templates/data/index/wiki-index.json +8 -0
- package/templates/qoder/agents/insight-planning.md +1 -1
- package/templates/qoder/agents/insight-research.md +28 -15
- package/templates/qoder/commands/optional/wl-insight.md +159 -161
- package/templates/qoder/commands/optional/wl-report.md +4 -4
- package/templates/qoder/commands/optional/wl-status.md +2 -2
- package/templates/qoder/commands/wl-code.md +2 -2
- package/templates/qoder/commands/wl-design.md +2 -2
- package/templates/qoder/commands/wl-init.md +3 -3
- package/templates/qoder/commands/wl-prd.md +2 -2
- package/templates/qoder/commands/wl-req.md +43 -0
- package/templates/qoder/commands/wl-search.md +8 -8
- package/templates/qoder/commands/wl-task.md +17 -17
- package/templates/qoder/commands/wl-test.md +41 -15
- package/templates/qoder/config.yaml +17 -1
- package/templates/qoder/hooks/session-start.py +12 -22
- package/templates/qoder/nul +4 -0
- package/templates/qoder/rules/wl-pipeline.md +22 -48
- package/templates/qoder/scripts/README.md +139 -0
- package/templates/qoder/scripts/common/autotest_auth.py +109 -0
- package/templates/qoder/scripts/common/bootstrap.py +145 -0
- package/templates/qoder/scripts/common/check_publish.py +98 -0
- package/templates/qoder/scripts/common/cmd_registry.py +112 -0
- package/templates/qoder/scripts/common/config.py +187 -0
- package/templates/qoder/scripts/common/contract.py +317 -0
- package/templates/qoder/scripts/common/developer.py +2 -1
- package/templates/qoder/scripts/common/feishu.py +10 -9
- package/templates/qoder/scripts/common/guard.py +159 -0
- package/templates/qoder/scripts/common/identity.py +121 -2
- package/templates/qoder/scripts/common/kg_capabilities.py +182 -0
- package/templates/qoder/scripts/common/mcp_base.py +268 -0
- package/templates/qoder/scripts/common/paths.py +187 -1
- package/templates/qoder/scripts/common/result.py +223 -0
- package/templates/qoder/scripts/common/roles.py +60 -0
- package/templates/qoder/scripts/common/task_utils.py +21 -9
- package/templates/qoder/scripts/common/test_extract.py +115 -0
- package/templates/qoder/scripts/kg/__init__.py +11 -0
- package/templates/qoder/scripts/kg/build_entity_registry.py +196 -0
- package/templates/qoder/scripts/kg/build_relations.py +127 -0
- package/templates/qoder/scripts/{build_style_index.py → kg/build_style_index.py} +39 -10
- package/templates/qoder/scripts/kg/build_workflows.py +144 -0
- package/templates/qoder/scripts/{context_pack.py → kg/context_pack.py} +18 -11
- package/templates/qoder/scripts/{enrich_prompt.py → kg/enrich_prompt.py} +232 -226
- package/templates/qoder/scripts/{extract_api_params.py → kg/extract.py} +398 -246
- package/templates/qoder/scripts/{kg.py → kg/kg.py} +638 -708
- package/templates/qoder/scripts/{kg_build.py → kg/kg_build.py} +618 -612
- package/templates/qoder/scripts/{kg_build_db.py → kg/kg_build_db.py} +333 -327
- package/templates/qoder/scripts/{kg_duckdb.py → kg/kg_duckdb.py} +38 -37
- package/templates/qoder/scripts/{kg_incremental.py → kg/kg_incremental.py} +420 -393
- package/templates/qoder/scripts/{kg_link_db.py → kg/kg_link_db.py} +230 -224
- package/templates/qoder/scripts/{kg_semantic.py → kg/kg_semantic.py} +156 -150
- package/templates/qoder/scripts/kg/prefetch.py +359 -0
- package/templates/qoder/scripts/{search_index.py → kg/search_index.py} +70 -14
- package/templates/qoder/scripts/mcp/__init__.py +11 -0
- package/templates/qoder/scripts/{kg_mcp_server.py → mcp/kg_mcp_server.py} +77 -272
- package/templates/qoder/scripts/{lanhu_stdio_wrapper.py → mcp/lanhu_stdio_wrapper.py} +125 -119
- package/templates/qoder/scripts/{check_mcp.py → mcp/mcp_doctor.py} +515 -298
- package/templates/qoder/scripts/{mcp_launcher.py → mcp/mcp_launcher.py} +442 -414
- package/templates/qoder/scripts/{mysql_mcp_server.py → mcp/mysql_mcp_server.py} +347 -396
- package/templates/qoder/scripts/{zentao_mcp_server.py → mcp/zentao_mcp_server.py} +384 -424
- package/templates/qoder/scripts/report/__init__.py +11 -0
- package/templates/qoder/scripts/{add_session.py → report/add_session.py} +250 -244
- package/templates/qoder/scripts/{archive_prd.py → report/archive_prd.py} +383 -377
- package/templates/qoder/scripts/{eval_prd.py → report/eval_prd.py} +73 -11
- package/templates/qoder/scripts/{export.py → report/export.py} +63 -0
- package/templates/qoder/scripts/{fill_prototype.py → report/fill_prototype.py} +6 -0
- package/templates/qoder/scripts/{gen_design_doc.py → report/gen_design_doc.py} +400 -394
- package/templates/qoder/scripts/{learn.py → report/learn.py} +152 -146
- package/templates/qoder/scripts/{learn_aggregate.py → report/learn_aggregate.py} +207 -201
- package/templates/qoder/scripts/{report.py → report/report.py} +287 -281
- package/templates/qoder/scripts/report/req.py +222 -0
- package/templates/qoder/scripts/report/role.py +33 -0
- package/templates/qoder/scripts/{status.py → report/status.py} +634 -628
- package/templates/qoder/scripts/setup/__init__.py +11 -0
- package/templates/qoder/scripts/setup/carriers.py +662 -0
- package/templates/qoder/scripts/{init_doctor.py → setup/init_doctor.py} +63 -26
- package/templates/qoder/scripts/{install_qoderwork.py → setup/install_qoderwork.py} +36 -26
- package/templates/qoder/scripts/{platform_doctor.py → setup/platform_doctor.py} +265 -259
- package/templates/qoder/scripts/{repo_root.py → setup/repo_root.py} +112 -106
- package/templates/qoder/scripts/{setup.py → setup/setup.py} +147 -0
- package/templates/qoder/scripts/{setup_lanhu.py → setup/setup_lanhu.py} +973 -963
- package/templates/qoder/scripts/task/__init__.py +11 -0
- package/templates/qoder/scripts/{git_sync.py → task/git_sync.py} +52 -31
- package/templates/qoder/scripts/{syncgate.py → task/syncgate.py} +6 -0
- package/templates/qoder/scripts/task/task.py +221 -0
- package/templates/qoder/scripts/task/task_lifecycle.py +596 -0
- package/templates/qoder/scripts/task/task_query.py +161 -0
- package/templates/qoder/scripts/task/task_relations.py +424 -0
- package/templates/qoder/scripts/{team_sync.py → task/team_sync.py} +93 -20
- package/templates/qoder/scripts/test/__init__.py +11 -0
- package/templates/qoder/scripts/{autotest.py → test/autotest.py} +1174 -1751
- package/templates/qoder/scripts/{autotest_batch.py → test/autotest_batch.py} +242 -224
- package/templates/qoder/scripts/test/autotest_data.py +675 -0
- package/templates/qoder/scripts/{autotest_run.py → test/autotest_run.py} +309 -297
- package/templates/qoder/scripts/{benchmark.py → test/benchmark.py} +6 -0
- package/templates/qoder/scripts/{kg_auto_login.py → test/kg_auto_login.py} +202 -196
- package/templates/qoder/scripts/{kg_test_runner.py → test/kg_test_runner.py} +7 -1
- package/templates/qoder/scripts/{page_probe.py → test/page_probe.py} +465 -459
- package/templates/qoder/scripts/wlkj.py +116 -0
- package/templates/qoder/settings.json +1 -10
- package/templates/qoder/skills/design-import/SKILL.md +226 -226
- package/templates/qoder/skills/design-review/SKILL.md +82 -82
- package/templates/qoder/skills/prd-generator/SKILL.md +26 -16
- package/templates/qoder/skills/prd-review/SKILL.md +5 -5
- package/templates/qoder/skills/prototype-generator/SKILL.md +256 -256
- package/templates/qoder/skills/spec-coder/SKILL.md +4 -4
- package/templates/qoder/skills/spec-generator/SKILL.md +4 -4
- package/templates/qoder/skills/test-generator/SKILL.md +5 -5
- package/templates/qoder/skills/wl-code/SKILL.md +4 -4
- package/templates/qoder/skills/wl-commit/SKILL.md +4 -4
- package/templates/qoder/skills/wl-design/SKILL.md +3 -3
- package/templates/qoder/skills/wl-init/SKILL.md +8 -8
- package/templates/qoder/skills/wl-insight/SKILL.md +5 -5
- package/templates/qoder/skills/wl-prd-full/SKILL.md +6 -6
- package/templates/qoder/skills/wl-prd-quick/SKILL.md +6 -6
- package/templates/qoder/skills/wl-prd-review/SKILL.md +4 -4
- package/templates/qoder/skills/wl-report/SKILL.md +7 -7
- package/templates/qoder/skills/wl-search/SKILL.md +13 -13
- package/templates/qoder/skills/wl-spec/SKILL.md +5 -5
- package/templates/qoder/skills/wl-status/SKILL.md +5 -5
- package/templates/qoder/skills/wl-task/SKILL.md +6 -6
- package/templates/qoder/skills/wl-test/SKILL.md +102 -39
- package/templates/root/AGENTS.md +39 -40
- package/templates/qoder/hooks/inject-workflow-state.py +0 -169
- package/templates/qoder/scripts/__pycache__/check_mcp_launch.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/__pycache__/install_qoderwork.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/__pycache__/mcp_launcher.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/__pycache__/platform_doctor.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/check_carriers.py +0 -238
- package/templates/qoder/scripts/check_mcp_launch.py +0 -183
- package/templates/qoder/scripts/check_qoderwork_consistency.py +0 -166
- package/templates/qoder/scripts/collect_prds.py +0 -31
- package/templates/qoder/scripts/common/mentions.py +0 -134
- package/templates/qoder/scripts/common/utf8.py +0 -38
- package/templates/qoder/scripts/extract_routes.py +0 -54
- package/templates/qoder/scripts/extract_routes_tree.py +0 -78
- package/templates/qoder/scripts/handoff.py +0 -22
- package/templates/qoder/scripts/init_developer.py +0 -76
- package/templates/qoder/scripts/parse_prds.py +0 -33
- package/templates/qoder/scripts/role.py +0 -51
- package/templates/qoder/scripts/sync_carriers.py +0 -259
- package/templates/qoder/scripts/task.py +0 -1261
- package/templates/qoder/scripts/workspace_init.py +0 -102
- package/templates/qoder/skills/prompt-enrich/SKILL.md +0 -90
- package/templates/qoder/skills/prototype-generator/SKILL.md.zcode-79180-2af4721f-f9a6-412c-88db-c0af680d211b.tmp +0 -0
- /package/templates/qoder/scripts/{secure-ls.js → test/secure-ls.js} +0 -0
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env python
|
|
2
2
|
# -*- coding: utf-8 -*-
|
|
3
|
+
# v3.0 路径自举: 引导到 common/bootstrap, 统一 sys.path 逻辑
|
|
4
|
+
import os as _o, sys as _s
|
|
5
|
+
_cp = _o.path.join(_o.path.dirname(_o.path.dirname(_o.path.abspath(__file__))), 'common')
|
|
6
|
+
if _cp not in _s.path: _s.path.insert(0, _cp)
|
|
7
|
+
from bootstrap import setup; setup()
|
|
8
|
+
|
|
3
9
|
"""
|
|
4
10
|
Knowledge Graph MCP Server for QoderWork (pure Python, no SDK dependency).
|
|
5
11
|
|
|
@@ -27,18 +33,22 @@ except Exception:
|
|
|
27
33
|
pass
|
|
28
34
|
|
|
29
35
|
# Locate the scripts dir
|
|
30
|
-
SCRIPTS_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
31
|
-
BASE_DIR = os.path.dirname(os.path.dirname(SCRIPTS_DIR)) # repo root (.qoder 的上级)
|
|
36
|
+
SCRIPTS_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # 子包→scripts/根
|
|
37
|
+
BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(SCRIPTS_DIR))) # 多一层: 子包→scripts→repo # repo root (.qoder 的上级)
|
|
32
38
|
sys.path.insert(0, SCRIPTS_DIR)
|
|
39
|
+
_KG_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
40
|
+
if _KG_DIR not in sys.path: sys.path.insert(0, _KG_DIR) # 同包import
|
|
33
41
|
|
|
34
|
-
#
|
|
35
|
-
#
|
|
36
|
-
_real_stdout = sys.stdout
|
|
37
|
-
sys.stdout = sys.stderr # default: prints go to stderr
|
|
42
|
+
# stdout 重定向 + 协议层由 BaseMCPServer 接管 (v3.0 重构)
|
|
43
|
+
# _real_stdout 不再模块级定义, 改为运行时从基类取 (见 _capture)
|
|
38
44
|
|
|
39
45
|
|
|
40
46
|
def _capture(func, *args, **kwargs):
|
|
41
|
-
"""Run a function, capture its stdout (which the scripts print to).
|
|
47
|
+
"""Run a function, capture its stdout (which the scripts print to).
|
|
48
|
+
|
|
49
|
+
v3.0: sys.stdout 在 run() 后已是 stderr (基类 _setup_streams 重定向),
|
|
50
|
+
_real_stdout 存在 BaseMCPServer._real_stdout。这里捕获时临时换回 buf。
|
|
51
|
+
"""
|
|
42
52
|
old = sys.stdout
|
|
43
53
|
buf = io.StringIO()
|
|
44
54
|
sys.stdout = buf
|
|
@@ -246,34 +256,38 @@ TOOLS = [
|
|
|
246
256
|
|
|
247
257
|
|
|
248
258
|
# Role-based section filtering for context_pack output
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
'admin': {'4', '5'}, # Admin: PRD+API overview
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
def _filter_by_role(text, role):
|
|
258
|
-
"""Filter context_pack output by role: keep only relevant sections."""
|
|
259
|
+
# v3.0: 单一真源移到 common/roles.py, 消除与 kg.py 的逐字重复
|
|
260
|
+
try:
|
|
261
|
+
from common.roles import ROLE_SECTIONS, filter_by_role as _filter_by_role
|
|
262
|
+
except Exception:
|
|
263
|
+
# 回退: 内联定义 (common 不可用时)
|
|
259
264
|
import re
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
265
|
+
ROLE_SECTIONS = {
|
|
266
|
+
'pm': {'1', '2', '2b', '3', '4', '5', '7'},
|
|
267
|
+
'design': {'2', '2b', '3', '6'},
|
|
268
|
+
'dev': {'1', '3', '5', '7'},
|
|
269
|
+
'test': {'1', '4', '5'},
|
|
270
|
+
'admin': {'4', '5'},
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
def _filter_by_role(text, role):
|
|
274
|
+
visible = ROLE_SECTIONS.get((role or '').lower())
|
|
275
|
+
if not visible:
|
|
276
|
+
return text
|
|
277
|
+
sections = re.split(r'(\n## \d+[ab]?\.)', text)
|
|
278
|
+
result = sections[0]
|
|
279
|
+
i = 1
|
|
280
|
+
while i < len(sections):
|
|
281
|
+
header = sections[i]
|
|
282
|
+
num_m = re.match(r'\n## (\d+[ab]?)\.', header)
|
|
283
|
+
if num_m and num_m.group(1) in visible:
|
|
284
|
+
result += header
|
|
285
|
+
if i + 1 < len(sections):
|
|
286
|
+
result += sections[i + 1]
|
|
287
|
+
i += 2
|
|
288
|
+
else:
|
|
289
|
+
i += 2
|
|
290
|
+
return result.strip()
|
|
277
291
|
|
|
278
292
|
|
|
279
293
|
# ── Tool implementations (added 2026-06-17) ──────────────────────────
|
|
@@ -281,107 +295,27 @@ def _filter_by_role(text, role):
|
|
|
281
295
|
# search_wiki 走 wiki-index.json(730词)。均不依赖已失效的旧 JSON 索引。
|
|
282
296
|
|
|
283
297
|
def _tool_get_workflow(module):
|
|
284
|
-
"""
|
|
285
|
-
|
|
286
|
-
审核: workflows 表 168 行/13 模块, 脏数据约 25 行(endpoint='None' 或
|
|
287
|
-
button 泄漏模板语法 '{{'), 清洗后链路完整。模块名均为英文, 中文输入会
|
|
288
|
-
模糊查 entities.cn 返回候选。
|
|
289
|
-
"""
|
|
290
|
-
import kg_duckdb as K
|
|
298
|
+
"""业务操作链查询 — v3.0 f3: 转发到 common.kg_capabilities (与 kg.py 共用单一真源)。"""
|
|
291
299
|
q = (module or '').strip()
|
|
292
300
|
if not q:
|
|
293
301
|
return "请提供模块名。可选: assets/cost/hr/maintenance/operation/qualityAndSafety/veh/settings/dashboard/flow/systems/monitor/quasafe"
|
|
302
|
+
import kg_duckdb as K
|
|
303
|
+
from common.kg_capabilities import workflow_query
|
|
294
304
|
con = K.get_db()
|
|
295
305
|
try:
|
|
296
|
-
|
|
297
|
-
hits = con.execute(
|
|
298
|
-
"SELECT DISTINCT module FROM workflows WHERE module = ? OR module LIKE '%' || ? || '%' ORDER BY module",
|
|
299
|
-
[q.lower(), q.lower()]
|
|
300
|
-
).fetchall()
|
|
301
|
-
if not hits:
|
|
302
|
-
# 2) 中文兜底: 查 entities 里 cn 匹配的, 给候选
|
|
303
|
-
cn_hits = con.execute(
|
|
304
|
-
"SELECT canonical, cn FROM entities WHERE type IN ('FEATURE','MODULE') AND cn <> '' AND (cn LIKE '%' || ? OR cn LIKE '%' || ? || '%')",
|
|
305
|
-
[q, q]
|
|
306
|
-
).fetchall()
|
|
307
|
-
if cn_hits:
|
|
308
|
-
cands = ', '.join('%s(%s)' % (c[0], c[1]) for c in cn_hits[:8])
|
|
309
|
-
return "未在 workflows 表找到模块 '%s'。但 entities 表有相近的中文名, 可换这些英文模块名重试:\n %s" % (q, cands)
|
|
310
|
-
all_mods = [r[0] for r in con.execute("SELECT DISTINCT module FROM workflows ORDER BY module").fetchall()]
|
|
311
|
-
return "未找到模块 '%s'。现有模块: %s" % (q, ', '.join(all_mods))
|
|
312
|
-
|
|
313
|
-
out = []
|
|
314
|
-
for (mod,) in hits:
|
|
315
|
-
# 清洗: 过滤空端点和模板泄漏按钮
|
|
316
|
-
rows = con.execute("""
|
|
317
|
-
SELECT state, seq, button, endpoint FROM workflows
|
|
318
|
-
WHERE module = ?
|
|
319
|
-
AND endpoint <> 'None' AND endpoint <> ''
|
|
320
|
-
AND button NOT LIKE '{{%'
|
|
321
|
-
ORDER BY seq, state
|
|
322
|
-
""", [mod]).fetchall()
|
|
323
|
-
if not rows:
|
|
324
|
-
continue
|
|
325
|
-
# 按 seq 分组展示操作链
|
|
326
|
-
out.append('=== %s (%d 步, 已清洗脏数据) ===' % (mod, len(rows)))
|
|
327
|
-
cur_seq = -1
|
|
328
|
-
for st, sq, btn, ep in rows:
|
|
329
|
-
if sq != cur_seq:
|
|
330
|
-
out.append(' [%d] %s' % (sq, st))
|
|
331
|
-
cur_seq = sq
|
|
332
|
-
out.append(' %-16s -> %s' % (btn, ep))
|
|
333
|
-
out.append('')
|
|
334
|
-
return '\n'.join(out).strip() or "模块 '%s' 清洗后无有效操作链" % q
|
|
306
|
+
return workflow_query(con, q)
|
|
335
307
|
finally:
|
|
336
308
|
try: con.close()
|
|
337
309
|
except Exception: pass
|
|
338
310
|
|
|
339
311
|
|
|
340
312
|
def _tool_search_wiki(keyword):
|
|
341
|
-
"""Repo Wiki
|
|
342
|
-
|
|
343
|
-
审核: repowiki.search_wiki 函数在, wiki-index.json 有 730 关键词。
|
|
344
|
-
search_index.search_wiki_cli 会在索引缺失时调 build_wiki_index (可能跑很久),
|
|
345
|
-
这里绕开它直接调底层 search_wiki, 索引缺失即报错让用户手动跑。
|
|
346
|
-
"""
|
|
313
|
+
"""Repo Wiki 搜索 — v3.0 f3: 转发到 common.kg_capabilities.wiki_query (CLI/MCP共用)。"""
|
|
347
314
|
kw = (keyword or '').strip()
|
|
348
315
|
if not kw:
|
|
349
316
|
return "请提供搜索关键词"
|
|
350
|
-
from common.
|
|
351
|
-
|
|
352
|
-
if not os.path.isfile(wiki_path):
|
|
353
|
-
return ("wiki-index.json 不存在。请在终端手动构建(一次性):\n"
|
|
354
|
-
" python .qoder/scripts/search_index.py wiki\n"
|
|
355
|
-
"(需要先在 Qoder IDE 对项目生成 Repo Wiki)")
|
|
356
|
-
try:
|
|
357
|
-
from common.repowiki import search_wiki
|
|
358
|
-
from common.terms import get_cn_map_with_auto
|
|
359
|
-
except ImportError as e:
|
|
360
|
-
return "Wiki 搜索模块缺失: %s" % str(e)
|
|
361
|
-
code_dir = os.path.join(_base, 'data', 'code')
|
|
362
|
-
try:
|
|
363
|
-
cn_map = get_cn_map_with_auto()
|
|
364
|
-
results = search_wiki(kw, str(_idx), code_dir, cn_map)
|
|
365
|
-
except Exception as e:
|
|
366
|
-
return "Wiki 搜索出错: %s" % str(e)
|
|
367
|
-
if not results:
|
|
368
|
-
return "No wiki pages match: %s" % kw
|
|
369
|
-
out = ['Wiki pages matching: %s (%d found)' % (kw, len(results)), '']
|
|
370
|
-
for i, r in enumerate(results, 1):
|
|
371
|
-
out.append(' %d. [%s] %s' % (i, r.get('project', '?'), r.get('title', '')))
|
|
372
|
-
deps = r.get('dependent_files') or []
|
|
373
|
-
if deps:
|
|
374
|
-
out.append(' 依赖: %s%s' % (', '.join(deps[:2]),
|
|
375
|
-
(' (共 %d 个文件)' % len(deps)) if len(deps) > 2 else ''))
|
|
376
|
-
if r.get('md_path'):
|
|
377
|
-
try:
|
|
378
|
-
rel = os.path.relpath(r['md_path'], _base)
|
|
379
|
-
except ValueError:
|
|
380
|
-
rel = r['md_path']
|
|
381
|
-
out.append(' 文档: %s' % rel)
|
|
382
|
-
out.append('')
|
|
383
|
-
out.append(' 提示: 读 wiki 文档 (带行号引用) 比读源码更省 token')
|
|
384
|
-
return '\n'.join(out)
|
|
317
|
+
from common.kg_capabilities import wiki_query
|
|
318
|
+
return wiki_query(kw)
|
|
385
319
|
|
|
386
320
|
|
|
387
321
|
def _tool_multi_hop(symbol, max_depth=3):
|
|
@@ -402,70 +336,9 @@ def _tool_multi_hop(symbol, max_depth=3):
|
|
|
402
336
|
depth = max(1, min(5, depth)) # 钳制 1-5
|
|
403
337
|
con = K.get_db()
|
|
404
338
|
try:
|
|
405
|
-
#
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
resolved = con.execute(
|
|
409
|
-
"SELECT entity_id FROM aliases WHERE alias = ?", [sym.lower()]
|
|
410
|
-
).fetchone()
|
|
411
|
-
if resolved:
|
|
412
|
-
start_id = resolved[0]
|
|
413
|
-
else:
|
|
414
|
-
# 1b) entity_id 模糊匹配 (用户传端点 /asset/list 或函数名)
|
|
415
|
-
fuzzy = con.execute(
|
|
416
|
-
"SELECT id FROM entities WHERE id = ? OR id LIKE '%' || ? || '%' LIMIT 1",
|
|
417
|
-
[sym, sym]
|
|
418
|
-
).fetchone()
|
|
419
|
-
if fuzzy:
|
|
420
|
-
start_id = fuzzy[0]
|
|
421
|
-
else:
|
|
422
|
-
# 1c) edges 里直接含该符号作为端点
|
|
423
|
-
edge_hit = con.execute(
|
|
424
|
-
"SELECT from_id FROM edges WHERE from_id LIKE '%' || ? || '%' LIMIT 1",
|
|
425
|
-
[sym]
|
|
426
|
-
).fetchone()
|
|
427
|
-
if edge_hit:
|
|
428
|
-
start_id = edge_hit[0]
|
|
429
|
-
|
|
430
|
-
# 2) 递归 CTE 遍历
|
|
431
|
-
try:
|
|
432
|
-
hops = K.multi_hop(con, start_id, max_depth=depth)
|
|
433
|
-
except Exception as e:
|
|
434
|
-
return "遍历出错 (起点=%s): %s" % (start_id, str(e))
|
|
435
|
-
|
|
436
|
-
if not hops or (len(hops) == 1 and hops[0][1] == 0):
|
|
437
|
-
return "从 '%s' (解析为 %s) 出发无可达节点。可能该符号不在图谱中, 或无出边。" % (sym, start_id)
|
|
438
|
-
|
|
439
|
-
# 3) 按 depth 分组, 取每层的实体详情 (最多展示)
|
|
440
|
-
from collections import defaultdict
|
|
441
|
-
by_depth = defaultdict(list)
|
|
442
|
-
for eid, d in hops:
|
|
443
|
-
by_depth[d].append(eid)
|
|
444
|
-
|
|
445
|
-
out = ['=== 多跳遍历: %s (解析为 %s) ===' % (sym, start_id)]
|
|
446
|
-
out.append('可达 %d 个实体, 最大 %d 跳:' % (len(hops), max(by_depth.keys())))
|
|
447
|
-
out.append('')
|
|
448
|
-
for d in sorted(by_depth.keys()):
|
|
449
|
-
eids = by_depth[d]
|
|
450
|
-
out.append('[第 %d 跳] %d 个实体:' % (d, len(eids)))
|
|
451
|
-
# 拉每个实体的 canonical/cn 信息
|
|
452
|
-
shown = 0
|
|
453
|
-
for eid in eids:
|
|
454
|
-
if shown >= 12:
|
|
455
|
-
out.append(' ... 还有 %d 个' % (len(eids) - 12))
|
|
456
|
-
break
|
|
457
|
-
info = con.execute(
|
|
458
|
-
"SELECT type, canonical, cn FROM entities WHERE id = ?", [eid]
|
|
459
|
-
).fetchone()
|
|
460
|
-
if info:
|
|
461
|
-
t, c, cn = info
|
|
462
|
-
label = '%s / %s' % (c, cn) if cn and cn != c else c
|
|
463
|
-
out.append(' %-14s %-12s %s' % ('[%s]' % t, eid[:40], label))
|
|
464
|
-
else:
|
|
465
|
-
out.append(' %-14s %s' % ('[edge]', eid[:60]))
|
|
466
|
-
shown += 1
|
|
467
|
-
out.append('')
|
|
468
|
-
return '\n'.join(out).strip()
|
|
339
|
+
# v3.0 f3: 核心查询转发到 common.kg_capabilities.hop_query (CLI/MCP共用)
|
|
340
|
+
from common.kg_capabilities import hop_query
|
|
341
|
+
return hop_query(con, sym, depth, multi_hop_fn=K.multi_hop)
|
|
469
342
|
finally:
|
|
470
343
|
try: con.close()
|
|
471
344
|
except Exception: pass
|
|
@@ -703,99 +576,31 @@ def _execute_tool(name, arguments):
|
|
|
703
576
|
return "未知工具: %s" % name
|
|
704
577
|
|
|
705
578
|
|
|
706
|
-
# ── MCP
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
"""Send a JSON-RPC message on stdout (MCP transport)."""
|
|
710
|
-
_real_stdout.write(json.dumps(msg, ensure_ascii=False) + '\n')
|
|
711
|
-
_real_stdout.flush()
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
async def _handle(req):
|
|
715
|
-
"""Handle a single JSON-RPC request."""
|
|
716
|
-
method = req.get("method", "")
|
|
717
|
-
req_id = req.get("id")
|
|
718
|
-
params = req.get("params", {})
|
|
719
|
-
|
|
720
|
-
if method == "initialize":
|
|
721
|
-
_send({
|
|
722
|
-
"jsonrpc": "2.0", "id": req_id,
|
|
723
|
-
"result": {
|
|
724
|
-
"protocolVersion": "2024-11-05",
|
|
725
|
-
"capabilities": {"tools": {}},
|
|
726
|
-
"serverInfo": {"name": "qoder-knowledge-graph", "version": "1.0.0"},
|
|
727
|
-
}
|
|
728
|
-
})
|
|
729
|
-
|
|
730
|
-
elif method == "notifications/initialized":
|
|
731
|
-
pass # no response needed for notifications
|
|
732
|
-
|
|
733
|
-
elif method == "tools/list":
|
|
734
|
-
_send({"jsonrpc": "2.0", "id": req_id, "result": {"tools": TOOLS}})
|
|
735
|
-
|
|
736
|
-
elif method == "tools/call":
|
|
737
|
-
tool_name = params.get("name", "")
|
|
738
|
-
arguments = params.get("arguments", {})
|
|
739
|
-
try:
|
|
740
|
-
result_text = await asyncio.get_event_loop().run_in_executor(
|
|
741
|
-
None, _execute_tool, tool_name, arguments)
|
|
742
|
-
_send({
|
|
743
|
-
"jsonrpc": "2.0", "id": req_id,
|
|
744
|
-
"result": {
|
|
745
|
-
"content": [{"type": "text", "text": result_text}],
|
|
746
|
-
"isError": False,
|
|
747
|
-
}
|
|
748
|
-
})
|
|
749
|
-
except Exception as e:
|
|
750
|
-
_send({
|
|
751
|
-
"jsonrpc": "2.0", "id": req_id,
|
|
752
|
-
"result": {
|
|
753
|
-
"content": [{"type": "text", "text": "[error] %s" % str(e)}],
|
|
754
|
-
"isError": True,
|
|
755
|
-
}
|
|
756
|
-
})
|
|
579
|
+
# ── MCP Server (继承 BaseMCPServer, 协议层由基类接管) ──────────────────
|
|
580
|
+
# v3.0 重构: 删除 _send/_handle/main 三段逐字复制代码 (~96行),
|
|
581
|
+
# 改为继承 BaseMCPServer。kg 的 initialize 不做内网校验 (本地知识图谱)。
|
|
757
582
|
|
|
758
|
-
|
|
759
|
-
|
|
583
|
+
def _bootstrap_base():
|
|
584
|
+
"""延迟 import BaseMCPServer (scripts/ 在 sys.path)。"""
|
|
585
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
586
|
+
from common.mcp_base import BaseMCPServer
|
|
587
|
+
return BaseMCPServer
|
|
760
588
|
|
|
761
|
-
else:
|
|
762
|
-
if req_id is not None:
|
|
763
|
-
_send({"jsonrpc": "2.0", "id": req_id,
|
|
764
|
-
"error": {"code": -32601, "message": "Method not found: %s" % method}})
|
|
765
589
|
|
|
590
|
+
class KnowledgeGraphMCPServer(_bootstrap_base()):
|
|
591
|
+
"""知识图谱 MCP Server。继承基类协议层, 只填工具逻辑。
|
|
766
592
|
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
Synchronous is more reliable than asyncio on Windows Proactor event loop.
|
|
771
|
-
Tool execution runs in a thread pool to avoid blocking on long queries.
|
|
593
|
+
与 mysql/zentao 不同: kg 不做内网校验 (本地 DuckDB),
|
|
594
|
+
所以不覆写 on_initialize_hook (基类默认空)。
|
|
772
595
|
"""
|
|
773
|
-
import threading
|
|
774
596
|
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
break
|
|
782
|
-
line = line.strip()
|
|
783
|
-
if not line:
|
|
784
|
-
continue
|
|
785
|
-
try:
|
|
786
|
-
req = json.loads(line.decode('utf-8'))
|
|
787
|
-
# Handle synchronously (tools are fast enough; context_pack ~1-2s)
|
|
788
|
-
loop = asyncio.new_event_loop()
|
|
789
|
-
loop.run_until_complete(_handle(req))
|
|
790
|
-
loop.close()
|
|
791
|
-
except json.JSONDecodeError:
|
|
792
|
-
pass
|
|
793
|
-
except Exception as e:
|
|
794
|
-
print('[kg-mcp] error: %s' % str(e), file=sys.stderr)
|
|
597
|
+
SERVER_NAME = "qoder-knowledge-graph"
|
|
598
|
+
SERVER_VERSION = "1.0.0"
|
|
599
|
+
TOOLS = TOOLS # 引用上方模块级 TOOLS (17个)
|
|
600
|
+
|
|
601
|
+
def handle_tool(self, name, args):
|
|
602
|
+
return _execute_tool(name, args)
|
|
795
603
|
|
|
796
604
|
|
|
797
605
|
if __name__ == "__main__":
|
|
798
|
-
|
|
799
|
-
main()
|
|
800
|
-
except KeyboardInterrupt:
|
|
801
|
-
pass
|
|
606
|
+
KnowledgeGraphMCPServer().run()
|