@hupan56/wlkj 2.7.12 → 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.
Files changed (152) hide show
  1. package/bin/cli.js +344 -78
  2. package/package.json +29 -29
  3. package/templates/.qoder/.runtime/ctx-cache-5660152f1d6dd819.md +23 -0
  4. package/templates/.qoder/.runtime/ctx-cache-afdce0dac06b25b0.md +23 -0
  5. package/templates/.qoder/.runtime/search-cache-eae7644e7b122f35.txt +1 -0
  6. package/templates/.qoder/learning/eval-history.jsonl +28 -0
  7. package/templates/data/index/wiki-index.json +8 -0
  8. package/templates/qoder/agents/insight-planning.md +1 -1
  9. package/templates/qoder/agents/insight-research.md +28 -15
  10. package/templates/qoder/commands/optional/wl-insight.md +159 -161
  11. package/templates/qoder/commands/optional/wl-report.md +4 -4
  12. package/templates/qoder/commands/optional/wl-status.md +2 -2
  13. package/templates/qoder/commands/wl-code.md +2 -2
  14. package/templates/qoder/commands/wl-design.md +2 -2
  15. package/templates/qoder/commands/wl-init.md +3 -3
  16. package/templates/qoder/commands/wl-prd.md +2 -2
  17. package/templates/qoder/commands/wl-req.md +43 -0
  18. package/templates/qoder/commands/wl-search.md +8 -8
  19. package/templates/qoder/commands/wl-task.md +17 -17
  20. package/templates/qoder/commands/wl-test.md +41 -15
  21. package/templates/qoder/config.yaml +17 -1
  22. package/templates/qoder/hooks/session-start.py +12 -22
  23. package/templates/qoder/nul +4 -0
  24. package/templates/qoder/rules/wl-pipeline.md +22 -48
  25. package/templates/qoder/scripts/README.md +139 -0
  26. package/templates/qoder/scripts/common/autotest_auth.py +109 -0
  27. package/templates/qoder/scripts/common/bootstrap.py +145 -0
  28. package/templates/qoder/scripts/common/check_publish.py +98 -0
  29. package/templates/qoder/scripts/common/cmd_registry.py +112 -0
  30. package/templates/qoder/scripts/common/config.py +187 -0
  31. package/templates/qoder/scripts/common/contract.py +317 -0
  32. package/templates/qoder/scripts/common/developer.py +2 -1
  33. package/templates/qoder/scripts/common/feishu.py +10 -9
  34. package/templates/qoder/scripts/common/guard.py +159 -0
  35. package/templates/qoder/scripts/common/identity.py +121 -2
  36. package/templates/qoder/scripts/common/kg_capabilities.py +182 -0
  37. package/templates/qoder/scripts/common/mcp_base.py +268 -0
  38. package/templates/qoder/scripts/common/paths.py +187 -1
  39. package/templates/qoder/scripts/common/result.py +223 -0
  40. package/templates/qoder/scripts/common/roles.py +60 -0
  41. package/templates/qoder/scripts/common/task_utils.py +21 -9
  42. package/templates/qoder/scripts/common/test_extract.py +115 -0
  43. package/templates/qoder/scripts/kg/__init__.py +11 -0
  44. package/templates/qoder/scripts/kg/build_entity_registry.py +196 -0
  45. package/templates/qoder/scripts/kg/build_relations.py +127 -0
  46. package/templates/qoder/scripts/{build_style_index.py → kg/build_style_index.py} +39 -10
  47. package/templates/qoder/scripts/kg/build_workflows.py +144 -0
  48. package/templates/qoder/scripts/{context_pack.py → kg/context_pack.py} +18 -11
  49. package/templates/qoder/scripts/{enrich_prompt.py → kg/enrich_prompt.py} +232 -226
  50. package/templates/qoder/scripts/{extract_api_params.py → kg/extract.py} +398 -246
  51. package/templates/qoder/scripts/{kg.py → kg/kg.py} +638 -708
  52. package/templates/qoder/scripts/{kg_build.py → kg/kg_build.py} +618 -612
  53. package/templates/qoder/scripts/{kg_build_db.py → kg/kg_build_db.py} +333 -327
  54. package/templates/qoder/scripts/{kg_duckdb.py → kg/kg_duckdb.py} +38 -37
  55. package/templates/qoder/scripts/{kg_incremental.py → kg/kg_incremental.py} +420 -393
  56. package/templates/qoder/scripts/{kg_link_db.py → kg/kg_link_db.py} +230 -224
  57. package/templates/qoder/scripts/{kg_semantic.py → kg/kg_semantic.py} +156 -150
  58. package/templates/qoder/scripts/kg/prefetch.py +359 -0
  59. package/templates/qoder/scripts/{search_index.py → kg/search_index.py} +70 -14
  60. package/templates/qoder/scripts/mcp/__init__.py +11 -0
  61. package/templates/qoder/scripts/{kg_mcp_server.py → mcp/kg_mcp_server.py} +77 -272
  62. package/templates/qoder/scripts/{lanhu_stdio_wrapper.py → mcp/lanhu_stdio_wrapper.py} +125 -119
  63. package/templates/qoder/scripts/{check_mcp.py → mcp/mcp_doctor.py} +515 -298
  64. package/templates/qoder/scripts/{mcp_launcher.py → mcp/mcp_launcher.py} +442 -414
  65. package/templates/qoder/scripts/{mysql_mcp_server.py → mcp/mysql_mcp_server.py} +347 -396
  66. package/templates/qoder/scripts/{zentao_mcp_server.py → mcp/zentao_mcp_server.py} +384 -424
  67. package/templates/qoder/scripts/report/__init__.py +11 -0
  68. package/templates/qoder/scripts/{add_session.py → report/add_session.py} +250 -244
  69. package/templates/qoder/scripts/{archive_prd.py → report/archive_prd.py} +383 -377
  70. package/templates/qoder/scripts/{eval_prd.py → report/eval_prd.py} +73 -11
  71. package/templates/qoder/scripts/{export.py → report/export.py} +63 -0
  72. package/templates/qoder/scripts/{fill_prototype.py → report/fill_prototype.py} +6 -0
  73. package/templates/qoder/scripts/{gen_design_doc.py → report/gen_design_doc.py} +400 -394
  74. package/templates/qoder/scripts/{learn.py → report/learn.py} +152 -146
  75. package/templates/qoder/scripts/{learn_aggregate.py → report/learn_aggregate.py} +207 -201
  76. package/templates/qoder/scripts/{report.py → report/report.py} +287 -281
  77. package/templates/qoder/scripts/report/req.py +222 -0
  78. package/templates/qoder/scripts/report/role.py +33 -0
  79. package/templates/qoder/scripts/{status.py → report/status.py} +634 -628
  80. package/templates/qoder/scripts/setup/__init__.py +11 -0
  81. package/templates/qoder/scripts/setup/carriers.py +662 -0
  82. package/templates/qoder/scripts/{init_doctor.py → setup/init_doctor.py} +63 -26
  83. package/templates/qoder/scripts/{install_qoderwork.py → setup/install_qoderwork.py} +36 -26
  84. package/templates/qoder/scripts/{platform_doctor.py → setup/platform_doctor.py} +265 -259
  85. package/templates/qoder/scripts/{repo_root.py → setup/repo_root.py} +112 -106
  86. package/templates/qoder/scripts/{setup.py → setup/setup.py} +113 -4
  87. package/templates/qoder/scripts/{setup_lanhu.py → setup/setup_lanhu.py} +973 -963
  88. package/templates/qoder/scripts/task/__init__.py +11 -0
  89. package/templates/qoder/scripts/{git_sync.py → task/git_sync.py} +52 -31
  90. package/templates/qoder/scripts/{syncgate.py → task/syncgate.py} +6 -0
  91. package/templates/qoder/scripts/task/task.py +221 -0
  92. package/templates/qoder/scripts/task/task_lifecycle.py +596 -0
  93. package/templates/qoder/scripts/task/task_query.py +161 -0
  94. package/templates/qoder/scripts/task/task_relations.py +424 -0
  95. package/templates/qoder/scripts/{team_sync.py → task/team_sync.py} +93 -20
  96. package/templates/qoder/scripts/test/__init__.py +11 -0
  97. package/templates/qoder/scripts/{autotest.py → test/autotest.py} +1174 -1751
  98. package/templates/qoder/scripts/{autotest_batch.py → test/autotest_batch.py} +242 -224
  99. package/templates/qoder/scripts/test/autotest_data.py +675 -0
  100. package/templates/qoder/scripts/{autotest_run.py → test/autotest_run.py} +309 -297
  101. package/templates/qoder/scripts/{benchmark.py → test/benchmark.py} +6 -0
  102. package/templates/qoder/scripts/{kg_auto_login.py → test/kg_auto_login.py} +202 -196
  103. package/templates/qoder/scripts/{kg_test_runner.py → test/kg_test_runner.py} +7 -1
  104. package/templates/qoder/scripts/{page_probe.py → test/page_probe.py} +465 -459
  105. package/templates/qoder/scripts/wlkj.py +116 -0
  106. package/templates/qoder/settings.json +1 -10
  107. package/templates/qoder/skills/design-import/SKILL.md +226 -226
  108. package/templates/qoder/skills/design-review/SKILL.md +82 -82
  109. package/templates/qoder/skills/prd-generator/SKILL.md +26 -16
  110. package/templates/qoder/skills/prd-review/SKILL.md +5 -5
  111. package/templates/qoder/skills/prototype-generator/SKILL.md +256 -256
  112. package/templates/qoder/skills/spec-coder/SKILL.md +4 -4
  113. package/templates/qoder/skills/spec-generator/SKILL.md +4 -4
  114. package/templates/qoder/skills/test-generator/SKILL.md +5 -5
  115. package/templates/qoder/skills/wl-code/SKILL.md +4 -4
  116. package/templates/qoder/skills/wl-commit/SKILL.md +4 -4
  117. package/templates/qoder/skills/wl-design/SKILL.md +3 -3
  118. package/templates/qoder/skills/wl-init/SKILL.md +8 -8
  119. package/templates/qoder/skills/wl-insight/SKILL.md +5 -5
  120. package/templates/qoder/skills/wl-prd-full/SKILL.md +6 -6
  121. package/templates/qoder/skills/wl-prd-quick/SKILL.md +6 -6
  122. package/templates/qoder/skills/wl-prd-review/SKILL.md +4 -4
  123. package/templates/qoder/skills/wl-report/SKILL.md +7 -7
  124. package/templates/qoder/skills/wl-search/SKILL.md +13 -13
  125. package/templates/qoder/skills/wl-spec/SKILL.md +5 -5
  126. package/templates/qoder/skills/wl-status/SKILL.md +5 -5
  127. package/templates/qoder/skills/wl-task/SKILL.md +6 -6
  128. package/templates/qoder/skills/wl-test/SKILL.md +102 -39
  129. package/templates/root/AGENTS.md +39 -40
  130. package/templates/qoder/hooks/inject-workflow-state.py +0 -169
  131. package/templates/qoder/scripts/__pycache__/check_mcp_launch.cpython-39.pyc +0 -0
  132. package/templates/qoder/scripts/__pycache__/install_qoderwork.cpython-39.pyc +0 -0
  133. package/templates/qoder/scripts/__pycache__/mcp_launcher.cpython-39.pyc +0 -0
  134. package/templates/qoder/scripts/__pycache__/platform_doctor.cpython-39.pyc +0 -0
  135. package/templates/qoder/scripts/check_carriers.py +0 -238
  136. package/templates/qoder/scripts/check_mcp_launch.py +0 -183
  137. package/templates/qoder/scripts/check_qoderwork_consistency.py +0 -166
  138. package/templates/qoder/scripts/collect_prds.py +0 -31
  139. package/templates/qoder/scripts/common/mentions.py +0 -134
  140. package/templates/qoder/scripts/common/utf8.py +0 -38
  141. package/templates/qoder/scripts/extract_routes.py +0 -54
  142. package/templates/qoder/scripts/extract_routes_tree.py +0 -78
  143. package/templates/qoder/scripts/handoff.py +0 -22
  144. package/templates/qoder/scripts/init_developer.py +0 -76
  145. package/templates/qoder/scripts/parse_prds.py +0 -33
  146. package/templates/qoder/scripts/role.py +0 -51
  147. package/templates/qoder/scripts/sync_carriers.py +0 -259
  148. package/templates/qoder/scripts/task.py +0 -1261
  149. package/templates/qoder/scripts/workspace_init.py +0 -102
  150. package/templates/qoder/skills/prompt-enrich/SKILL.md +0 -90
  151. package/templates/qoder/skills/prototype-generator/SKILL.md.zcode-79180-2af4721f-f9a6-412c-88db-c0af680d211b.tmp +0 -0
  152. /package/templates/qoder/scripts/{secure-ls.js → test/secure-ls.js} +0 -0
@@ -1,708 +1,638 @@
1
- # -*- coding: utf-8 -*-
2
- """
3
- kg.py QODER 知识图谱统一 CLI 入口(全系列通用)
4
-
5
- 把原本只在 QoderWork MCP (kg_mcp_server.py) 里的 13 个知识图谱能力,
6
- 统一成一套命令行子命令。Qoder IDE / Quest / CLI 直接跑;QoderWork 仍可
7
- 用 MCP,本脚本是新增的并列通道(功能等价)。
8
-
9
- 设计原则:
10
- 1. 不复制逻辑 — 调底层: search_index / context_pack / graph_traverse /
11
- kg_duckdb / repowiki / fill_prototype / gen_design_doc
12
- 2. 全容错 DuckDB/索引缺失优雅降级,给修复提示,绝不抛栈给用户
13
- 3. 参数统一 — 13 个子命令风格一致,对标 MCP 工具名
14
- 4. 纯文本输出 — UTF-8,无日志噪音,AI 直接读
15
-
16
- 用法:
17
- python .qoder/scripts/kg.py <子命令> [参数]
18
-
19
- python .qoder/scripts/kg.py search 考勤 --platform web
20
- python .qoder/scripts/kg.py impact /asset
21
- python .qoder/scripts/kg.py coverage
22
- python .qoder/scripts/kg.py feature 资产管理
23
- python .qoder/scripts/kg.py workflow assets
24
- python .qoder/scripts/kg.py hop 资产管理 --depth 3
25
- """
26
- import io
27
- import os
28
- import re
29
- import sys
30
-
31
- # UTF-8 输出(Windows 终端默认 GBK 会乱码)
32
- try:
33
- sys.stdout.reconfigure(encoding='utf-8', errors='replace')
34
- except Exception:
35
- pass
36
-
37
- SCRIPTS_DIR = os.path.dirname(os.path.abspath(__file__))
38
- BASE_DIR = os.path.dirname(os.path.dirname(SCRIPTS_DIR)) # repo root
39
- sys.path.insert(0, SCRIPTS_DIR)
40
-
41
-
42
- # ── 路径与索引新鲜度(与底层一致)──────────────────────────────────────
43
- def _index_dir():
44
- """获取 data/index 目录(延迟导入,避免顶层崩)。"""
45
- try:
46
- from common.paths import DATA_INDEX_DIR
47
- return str(DATA_INDEX_DIR)
48
- except Exception:
49
- # 兜底: 常规布局
50
- return os.path.join(BASE_DIR, 'data', 'index')
51
-
52
-
53
- def _kg_db_path():
54
- return os.path.join(_index_dir(), 'kg.duckdb')
55
-
56
-
57
- def _has_duckdb():
58
- """知识图谱 DuckDB 是否已构建。"""
59
- return os.path.isfile(_kg_db_path())
60
-
61
-
62
- def _hint_rebuild(action_cn='知识图谱'):
63
- """DuckDB/索引缺失时的统一修复提示。"""
64
- return (
65
- "⚠️ %s未构建或缺失。\n"
66
- "修复方法(任选其一):\n"
67
- " 1. 对 AI 说「初始化」(/wl-init) — 自动刷新索引\n"
68
- " 2. 手动构建: python .qoder/scripts/kg_build.py\n"
69
- " 3. 自检修复: python .qoder/scripts/init_doctor.py --fix\n"
70
- "本次查询无法继续,但其他子命令(不依赖图谱的)仍可用。"
71
- % action_cn
72
- )
73
-
74
-
75
- # ── 子命令实现 ────────────────────────────────────────────────────────
76
-
77
- def cmd_search(args):
78
- """搜代码: 按关键词找代码文件。"""
79
- if not args:
80
- return "用法: kg.py search <关键词> [--platform web|app]"
81
- kw, plat = _split_kw_platform(args)
82
- if not kw:
83
- return "请提供搜索关键词"
84
- import search_index as S
85
- # search_keywords(query, platform=None) 自带中文扩展,不需外层 expand
86
- return _capture(S.search_keywords, kw, plat) or "无匹配结果"
87
-
88
-
89
- def cmd_api(args):
90
- """搜 API 端点。"""
91
- if not args:
92
- return "用法: kg.py api <关键词>"
93
- import search_index as S
94
- return _capture(S.search_api, args[0]) or "无匹配 API"
95
-
96
-
97
- def cmd_prd(args):
98
- """搜 PRD(防重复造轮子)。"""
99
- if not args:
100
- return "用法: kg.py prd <关键词>"
101
- import search_index as S
102
- return _capture(S.search_prds, args[0]) or "无匹配 PRD"
103
-
104
-
105
- def cmd_impact(args):
106
- """影响分析: 改某接口影响哪些前端页面/按钮。"""
107
- if not args:
108
- return "用法: kg.py impact <端点或关键词> 例: /asset 或 export"
109
- import search_index as S
110
- return _capture(S.search_impact, args[0]) or "未找到匹配端点"
111
-
112
-
113
- def cmd_context360(args):
114
- """符号 360 度视图: 端点/函数/处理器的全部关联。"""
115
- if not args:
116
- return "用法: kg.py context360 <符号> 例: handleExport/system/role"
117
- import search_index as S
118
- return _capture(S.search_context360, args[0]) or "未找到匹配符号"
119
-
120
-
121
- def cmd_context(args):
122
- """上下文打包: 一次返回关键词相关的全部上下文。支持 --role 角色裁剪。"""
123
- if not args:
124
- return ("用法: kg.py context <关键词> [--platform web|app] "
125
- "[--role pm|design|dev|test|admin]")
126
- rest = list(args)
127
- role = ''
128
- plat = ''
129
- # 解析 --role / --platform
130
- rest2 = []
131
- i = 0
132
- while i < len(rest):
133
- a = rest[i]
134
- if a == '--role' and i + 1 < len(rest):
135
- role = rest[i + 1]; i += 2; continue
136
- if a == '--platform' and i + 1 < len(rest):
137
- plat = rest[i + 1]; i += 2; continue
138
- rest2.append(a); i += 1
139
- kw = rest2[0] if rest2 else ''
140
- if not kw:
141
- return "请提供业务关键词"
142
- import context_pack as C
143
- full = _capture(C._compute_and_print, kw, plat, '')
144
- if not full:
145
- return "无匹配上下文"
146
- if role:
147
- return _filter_by_role(full, role)
148
- return full
149
-
150
-
151
- def cmd_coverage(args):
152
- """功能×测试覆盖矩阵: 哪些功能有测试,哪些是盲区。"""
153
- from common.graph_traverse import CodeGraph
154
- g = CodeGraph(_index_dir())
155
- return g.coverage_matrix() or "无法生成覆盖矩阵(索引可能为空)"
156
-
157
-
158
- def cmd_feature(args):
159
- """功能画像: 一个功能的完整画像(端点+按钮+测试)。
160
-
161
- DuckDB 优先(DuckDB 数据最全);DuckDB 缺失时回退到 CodeGraph 的 JSON 路径。
162
- """
163
- if not args:
164
- return "用法: kg.py feature <功能名> 例: 资产管理 或 asset 或 system"
165
- feat = args[0]
166
- # DuckDB 优先(数据最全,coverage 已验证可用)
167
- if _has_duckdb():
168
- try:
169
- import duckdb
170
- except ImportError:
171
- return "未安装 duckdb。请运行: pip install duckdb"
172
- con = duckdb.connect(_kg_db_path(), read_only=True)
173
- try:
174
- result = _feature_duckdb(con, feat)
175
- if result:
176
- return result
177
- finally:
178
- try: con.close()
179
- except Exception: pass
180
- # 回退: CodeGraph 的 JSON 实现(entity-registry.json 通常不存在)
181
- from common.graph_traverse import CodeGraph
182
- g = CodeGraph(_index_dir())
183
- return g.feature_overview(feat) or "未找到功能 '%s'" % feat
184
-
185
-
186
- def _feature_duckdb(con, query):
187
- """从 DuckDB 查功能画像(feature endpoints/buttons/tests)。
188
-
189
- 别名解析: aliases 精确 → canonical/cn 模糊 → entity_id 含关键词。
190
- """
191
- q = (query or '').strip()
192
- # 1) 解析为 FEATURE entity_id
193
- target = None
194
- resolved = con.execute(
195
- "SELECT entity_id FROM aliases WHERE alias = ? "
196
- "AND entity_id IN (SELECT id FROM entities WHERE type='FEATURE')",
197
- [q.lower()]
198
- ).fetchone()
199
- if resolved:
200
- target = resolved[0]
201
- else:
202
- # canonical / cn 模糊
203
- row = con.execute(
204
- "SELECT id FROM entities WHERE type='FEATURE' "
205
- "AND (LOWER(canonical)=? OR cn=? OR LOWER(canonical) LIKE '%'||?||'%' OR cn LIKE '%'||?||'%') "
206
- "LIMIT 1",
207
- [q.lower(), q, q.lower(), q]
208
- ).fetchone()
209
- if row:
210
- target = row[0]
211
- if not target:
212
- # 列出可用 feature 帮用户选
213
- feats = con.execute(
214
- "SELECT canonical, cn FROM entities WHERE type='FEATURE' AND cn<>'' ORDER BY cn LIMIT 30"
215
- ).fetchall()
216
- cands = ', '.join('%s(%s)' % (c, cn) if cn and cn != c else c for c, cn in feats)
217
- return "未找到功能 '%s'。可用功能: %s" % (q, cands)
218
-
219
- # 2) 取 feature 基本信息
220
- info = con.execute(
221
- "SELECT canonical, cn FROM entities WHERE id=?", [target]
222
- ).fetchone()
223
- canon, cn = info if info else (target, target)
224
-
225
- # 3) 统计端点 / 按钮
226
- endpoints = con.execute(
227
- "SELECT to_id FROM edges WHERE from_id=? AND edge_type='has_endpoint'", [target]
228
- ).fetchall()
229
- buttons = con.execute(
230
- "SELECT to_id FROM edges WHERE from_id=? AND edge_type='has_button'", [target]
231
- ).fetchall()
232
-
233
- # 4) 关联测试: feature 的端点 -(handled_by)-> controller -(tested_by)-> test
234
- try:
235
- tests = con.execute("""
236
- SELECT DISTINCT tb.to_id FROM edges he
237
- JOIN edges tb ON tb.from_id = he.to_id AND tb.edge_type='tested_by'
238
- WHERE he.from_id=? AND he.edge_type='handled_by'
239
- """, [target]).fetchall()
240
- except Exception:
241
- tests = []
242
-
243
- ep_ids = [e[0] for e in endpoints]
244
- btn_ids = [b[0] for b in buttons]
245
- test_ids = [t[0] for t in tests]
246
-
247
- # 5) 组装画像
248
- out = ['## 功能画像: %s (%s)' % (cn, canon)]
249
- out.append(' 端点 %d | 按钮操作 %d | 关联测试 %d 个' % (
250
- len(ep_ids), len(btn_ids), len(test_ids)))
251
- out.append('')
252
-
253
- out.append('### 端点 (%d)' % len(ep_ids))
254
- for eid in ep_ids[:10]:
255
- out.append(' %s' % eid)
256
- if len(ep_ids) > 10:
257
- out.append(' ... 还有 %d 个' % (len(ep_ids) - 10))
258
- out.append('')
259
-
260
- out.append('### 按钮操作 (%d)' % len(btn_ids))
261
- # 提取按钮的操作动词(handleAdd/handleDel )
262
- verbs = {}
263
- for bid in btn_ids:
264
- # bid 形如 B:path/to/file.vue:handleAdd
265
- if ':' in bid:
266
- handler = bid.rsplit(':', 1)[-1]
267
- else:
268
- handler = bid
269
- verbs[handler] = verbs.get(handler, 0) + 1
270
- top_verbs = sorted(verbs.items(), key=lambda x: -x[1])[:15]
271
- out.append(' 操作分布: %s' % ', '.join('%s×%d' % (v, n) for v, n in top_verbs))
272
- out.append('')
273
-
274
- out.append('### 关联测试 (%d)' % len(test_ids))
275
- shown = 0
276
- for tid in test_ids:
277
- if shown >= 6:
278
- out.append(' ... 还有 %d 个' % (len(test_ids) - 6))
279
- break
280
- # tid 形如 T:ClassName.method,查 tests 表拿断言
281
- if tid.startswith('T:'):
282
- ref = tid[2:]
283
- cls = method = ''
284
- if '.' in ref:
285
- cls, method = ref.rsplit('.', 1)
286
- trow = con.execute(
287
- "SELECT display_name, assertions FROM tests WHERE class=? AND method=? LIMIT 1",
288
- [cls, method]
289
- ).fetchone()
290
- if trow:
291
- dn, asserts = trow
292
- import json as _json
293
- try:
294
- a_list = _json.loads(asserts) if isinstance(asserts, str) else asserts
295
- a_str = ', '.join(a_list[:3]) if a_list else ''
296
- except Exception:
297
- a_str = ''
298
- out.append(' %s — %s%s' % (ref, dn or '', (' [断言: %s]' % a_str) if a_str else ''))
299
- else:
300
- out.append(' %s' % ref)
301
- else:
302
- out.append(' %s' % tid)
303
- shown += 1
304
-
305
- if len(test_ids) == 0:
306
- out.append(' ⚠️ 该功能无关联测试,是回归测试盲区')
307
-
308
- return '\n'.join(out)
309
-
310
-
311
- def cmd_workflow(args):
312
- """业务流程链: 一个模块的完整操作链(query→create→audit→...)。
313
-
314
- 优先匹配英文模块名(assets/cost/hr/...),中文输入返回候选。
315
- 数据源: DuckDB workflows 表。
316
- """
317
- if not args:
318
- return ("用法: kg.py workflow <模块名> 例: assets / cost / hr\n"
319
- "可选模块: assets/cost/hr/maintenance/operation/"
320
- "qualityAndSafety/veh/settings/dashboard/flow/systems/monitor/quasafe")
321
- module = args[0]
322
- if not _has_duckdb():
323
- return _hint_rebuild('业务流程数据(DuckDB)')
324
- try:
325
- import duckdb
326
- except ImportError:
327
- return ("未安装 duckdb。请运行: pip install duckdb")
328
- con = duckdb.connect(_kg_db_path(), read_only=True)
329
- try:
330
- return _workflow_query(con, module)
331
- finally:
332
- try: con.close()
333
- except Exception: pass
334
-
335
-
336
- def _workflow_query(con, q):
337
- """workflows 表查询 + 脏数据清洗 + 中文兜底(从 kg_mcp_server 平移)。"""
338
- q = (q or '').strip()
339
- # 1) 精确 + 模糊匹配 workflows.module(英文)
340
- hits = con.execute(
341
- "SELECT DISTINCT module FROM workflows WHERE module = ? OR module LIKE '%' || ? || '%' ORDER BY module",
342
- [q.lower(), q.lower()]
343
- ).fetchall()
344
- if not hits:
345
- # 2) 中文兜底: entities.cn 返回英文候选
346
- cn_hits = con.execute(
347
- "SELECT canonical, cn FROM entities WHERE type IN ('FEATURE','MODULE') AND cn <> '' AND (cn LIKE '%' || ? OR cn LIKE '%' || ? || '%')",
348
- [q, q]
349
- ).fetchall()
350
- if cn_hits:
351
- cands = ', '.join('%s(%s)' % (c[0], c[1]) for c in cn_hits[:8])
352
- return "未在 workflows 表找到模块 '%s'。但 entities 表有相近中文名,可换这些英文模块名重试:\n %s" % (q, cands)
353
- all_mods = [r[0] for r in con.execute("SELECT DISTINCT module FROM workflows ORDER BY module").fetchall()]
354
- return "未找到模块 '%s'。现有模块: %s" % (q, ', '.join(all_mods))
355
-
356
- out = []
357
- for (mod,) in hits:
358
- # 清洗: 过滤空端点和模板泄漏按钮
359
- rows = con.execute("""
360
- SELECT state, seq, button, endpoint FROM workflows
361
- WHERE module = ?
362
- AND endpoint <> 'None' AND endpoint <> ''
363
- AND button NOT LIKE '{{%'
364
- ORDER BY seq, state
365
- """, [mod]).fetchall()
366
- if not rows:
367
- continue
368
- out.append('=== %s (%d 步, 已清洗脏数据) ===' % (mod, len(rows)))
369
- cur_seq = -1
370
- for st, sq, btn, ep in rows:
371
- if sq != cur_seq:
372
- out.append(' [%d] %s' % (sq, st))
373
- cur_seq = sq
374
- out.append(' %-16s -> %s' % (btn, ep))
375
- out.append('')
376
- return '\n'.join(out).strip() or "模块 '%s' 清洗后无有效操作链" % q
377
-
378
-
379
- def cmd_hop(args):
380
- """多跳遍历: 从一个符号出发,递归 N 跳找全部可达实体。
381
-
382
- 知识图谱核心能力。支持别名解析(传中文/端点/函数名自动转 entity_id)。
383
- """
384
- if not args:
385
- return "用法: kg.py hop <符号> [--depth 1-5] 例: 资产管理 或 /asset/list"
386
- symbol, depth = _split_symbol_depth(args)
387
- if not _has_duckdb():
388
- return _hint_rebuild('知识图谱(DuckDB)')
389
- try:
390
- import duckdb
391
- except ImportError:
392
- return "未安装 duckdb。请运行: pip install duckdb"
393
- import kg_duckdb as K
394
- con = duckdb.connect(_kg_db_path(), read_only=True)
395
- try:
396
- # 1) 别名解析(三级兜底)
397
- start_id = symbol
398
- resolved = con.execute(
399
- "SELECT entity_id FROM aliases WHERE alias = ?", [symbol.lower()]
400
- ).fetchone()
401
- if resolved:
402
- start_id = resolved[0]
403
- else:
404
- fuzzy = con.execute(
405
- "SELECT id FROM entities WHERE id = ? OR id LIKE '%' || ? || '%' LIMIT 1",
406
- [symbol, symbol]
407
- ).fetchone()
408
- if fuzzy:
409
- start_id = fuzzy[0]
410
- else:
411
- edge_hit = con.execute(
412
- "SELECT from_id FROM edges WHERE from_id LIKE '%' || ? || '%' LIMIT 1",
413
- [symbol]
414
- ).fetchone()
415
- if edge_hit:
416
- start_id = edge_hit[0]
417
-
418
- # 2) 递归 CTE 遍历
419
- try:
420
- hops = K.multi_hop(con, start_id, max_depth=depth)
421
- except Exception as e:
422
- return "遍历出错 (起点=%s): %s" % (start_id, str(e))
423
-
424
- if not hops or (len(hops) == 1 and hops[0][1] == 0):
425
- return "从 '%s' (解析为 %s) 出发无可达节点。可能该符号不在图谱中,或无出边。" % (symbol, start_id)
426
-
427
- # 3) 按 depth 分组展示
428
- from collections import defaultdict
429
- by_depth = defaultdict(list)
430
- for eid, d in hops:
431
- by_depth[d].append(eid)
432
-
433
- out = ['=== 多跳遍历: %s (解析为 %s) ===' % (symbol, start_id)]
434
- out.append('可达 %d 个实体, 最大 %d 跳:' % (len(hops), max(by_depth.keys())))
435
- out.append('')
436
- for d in sorted(by_depth.keys()):
437
- eids = by_depth[d]
438
- out.append('[第 %d 跳] %d 个实体:' % (d, len(eids)))
439
- shown = 0
440
- for eid in eids:
441
- if shown >= 12:
442
- out.append(' ... 还有 %d 个' % (len(eids) - 12))
443
- break
444
- info = con.execute(
445
- "SELECT type, canonical, cn FROM entities WHERE id = ?", [eid]
446
- ).fetchone()
447
- if info:
448
- t, c, cn = info
449
- label = '%s / %s' % (c, cn) if cn and cn != c else c
450
- out.append(' %-14s %-12s %s' % ('[%s]' % t, eid[:40], label))
451
- else:
452
- out.append(' %-14s %s' % ('[edge]', eid[:60]))
453
- shown += 1
454
- out.append('')
455
- return '\n'.join(out).strip()
456
- finally:
457
- try: con.close()
458
- except Exception: pass
459
-
460
-
461
- def cmd_wiki(args):
462
- """Repo Wiki 搜索: 查模块级语义文档(比读源码省 token)。
463
-
464
- 只读 wiki-index.json,不触发自动构建(慢操作)。
465
- """
466
- if not args:
467
- return "用法: kg.py wiki <关键词>"
468
- kw = args[0]
469
- from common.paths import DATA_INDEX_DIR as _idx, PROJECT_ROOT as _base
470
- wiki_path = os.path.join(str(_idx), 'wiki-index.json')
471
- if not os.path.isfile(wiki_path):
472
- return ("wiki-index.json 不存在。请在终端手动构建(一次性):\n"
473
- " python .qoder/scripts/search_index.py wiki\n"
474
- "(需要先在 Qoder IDE 对项目生成 Repo Wiki)")
475
- try:
476
- from common.repowiki import search_wiki
477
- from common.terms import get_cn_map_with_auto
478
- except ImportError as e:
479
- return "Wiki 搜索模块缺失: %s" % str(e)
480
- code_dir = os.path.join(_base, 'data', 'code')
481
- try:
482
- cn_map = get_cn_map_with_auto()
483
- results = search_wiki(kw, str(_idx), code_dir, cn_map)
484
- except Exception as e:
485
- return "Wiki 搜索出错: %s" % str(e)
486
- if not results:
487
- return "No wiki pages match: %s" % kw
488
- out = ['Wiki pages matching: %s (%d found)' % (kw, len(results)), '']
489
- for i, r in enumerate(results, 1):
490
- out.append(' %d. [%s] %s' % (i, r.get('project', '?'), r.get('title', '')))
491
- deps = r.get('dependent_files') or []
492
- if deps:
493
- out.append(' 依赖: %s%s' % (', '.join(deps[:2]),
494
- (' (共 %d 个文件)' % len(deps)) if len(deps) > 2 else ''))
495
- if r.get('md_path'):
496
- try:
497
- rel = os.path.relpath(r['md_path'], _base)
498
- except ValueError:
499
- rel = r['md_path']
500
- out.append(' 文档: %s' % rel)
501
- out.append('')
502
- out.append(' 提示: wiki 文档(带行号引用)比读源码更省 token')
503
- return '\n'.join(out)
504
-
505
-
506
- def cmd_fill_prototype(args):
507
- """原型预填: 用真实代码数据自动填模板,输出 80% 完成的 HTML 草稿。"""
508
- if not args:
509
- return "用法: kg.py fill-prototype <关键词> [--platform web|app]"
510
- kw, plat = _split_kw_platform(args)
511
- plat = plat or 'web'
512
- import subprocess as _sp
513
- script = os.path.join(SCRIPTS_DIR, 'fill_prototype.py')
514
- if not os.path.isfile(script):
515
- return "fill_prototype.py 不存在"
516
- try:
517
- r = _sp.run([sys.executable, script, kw, '--platform', plat],
518
- capture_output=True, text=True, encoding='utf-8',
519
- errors='replace', timeout=30, cwd=SCRIPTS_DIR)
520
- return r.stdout or "fill_prototype 无输出(可能索引缺失)"
521
- except Exception as e:
522
- return "[error] fill_prototype: %s" % str(e)
523
-
524
-
525
- def cmd_design_system(args):
526
- """设计系统查询: 返回项目完整设计规范(布局指纹/颜色token/组件表/图标)。"""
527
- plat = ''
528
- if args and args[0] in ('--platform', '-p') and len(args) > 1:
529
- plat = args[1]
530
- import subprocess as _sp
531
- design_md = os.path.join(BASE_DIR, 'data', 'design', 'DESIGN.md')
532
- if not os.path.isfile(design_md):
533
- gen_script = os.path.join(SCRIPTS_DIR, 'gen_design_doc.py')
534
- if os.path.isfile(gen_script):
535
- _sp.run([sys.executable, gen_script], capture_output=True, timeout=30)
536
- if os.path.isfile(design_md):
537
- content = open(design_md, encoding='utf-8').read()
538
- if plat:
539
- lines = content.split('\n')
540
- result = []
541
- in_section = False
542
- for line in lines:
543
- if line.startswith('### ') and plat.lower() in line.lower():
544
- in_section = True
545
- elif line.startswith('### ') and in_section:
546
- break
547
- if in_section:
548
- result.append(line)
549
- return '\n'.join(result) if result else content
550
- return content
551
- return "DESIGN.md 不存在且生成失败。请手动跑: python .qoder/scripts/gen_design_doc.py"
552
-
553
-
554
- # ── 工具函数 ──────────────────────────────────────────────────────────
555
-
556
- def _capture(func, *args, **kwargs):
557
- """运行函数,捕获其 stdout 输出(底层脚本用 print 输出)。"""
558
- old = sys.stdout
559
- buf = io.StringIO()
560
- sys.stdout = buf
561
- try:
562
- func(*args, **kwargs)
563
- except Exception as e:
564
- buf.write('\n[error] %s' % str(e))
565
- finally:
566
- sys.stdout = old
567
- return buf.getvalue().strip()
568
-
569
-
570
- def _split_kw_platform(args):
571
- """从参数里拆出 keyword 和 --platform 值。"""
572
- rest = list(args)
573
- plat = ''
574
- out = []
575
- i = 0
576
- while i < len(rest):
577
- a = rest[i]
578
- if a == '--platform' and i + 1 < len(rest):
579
- plat = rest[i + 1]; i += 2; continue
580
- out.append(a); i += 1
581
- kw = out[0] if out else ''
582
- return kw, plat
583
-
584
-
585
- def _split_symbol_depth(args):
586
- """从参数里拆出 symbol 和 --depth 值(默认 3)。"""
587
- rest = list(args)
588
- depth = 3
589
- out = []
590
- i = 0
591
- while i < len(rest):
592
- a = rest[i]
593
- if a == '--depth' and i + 1 < len(rest):
594
- try: depth = int(rest[i + 1])
595
- except (ValueError, TypeError): depth = 3
596
- i += 2; continue
597
- out.append(a); i += 1
598
- return (out[0] if out else ''), max(1, min(5, depth))
599
-
600
-
601
- # Role-based section filtering for context_pack output(与 MCP 一致)
602
- ROLE_SECTIONS = {
603
- 'pm': {'1', '2', '2b', '3', '4', '5', '7'}, # PM: 字段+PRD+API,无代码噪音
604
- 'design': {'2', '2b', '3', '6'}, # 设计: 页面+布局+字段+风格
605
- 'dev': {'1', '3', '5', '7'}, # 开发: 代码+字段+API+wiki
606
- 'test': {'1', '4', '5'}, # 测试: 代码+PRD+API
607
- 'admin': {'4', '5'}, # 管理: PRD+API 概览
608
- }
609
-
610
-
611
- def _filter_by_role(text, role):
612
- """按角色过滤 context_pack 输出的章节。"""
613
- visible = ROLE_SECTIONS.get((role or '').lower())
614
- if not visible:
615
- return text # 未知角色不过滤
616
- sections = re.split(r'(\n## \d+[ab]?\.)', text)
617
- result = sections[0]
618
- i = 1
619
- while i < len(sections):
620
- header = sections[i]
621
- num_m = re.match(r'\n## (\d+[ab]?)\.', header)
622
- if num_m and num_m.group(1) in visible:
623
- result += header
624
- if i + 1 < len(sections):
625
- result += sections[i + 1]
626
- i += 2
627
- else:
628
- i += 2
629
- return result.strip()
630
-
631
-
632
- # ── 子命令注册表 ──────────────────────────────────────────────────────
633
- # (子命令名, 处理函数, 中文说明)
634
- COMMANDS = [
635
- ('search', cmd_search, '搜代码: 按关键词找代码文件'),
636
- ('api', cmd_api, '搜 API 端点'),
637
- ('prd', cmd_prd, '搜 PRD(防重复造轮子)'),
638
- ('impact', cmd_impact, '影响分析: 改某接口影响哪些页面/按钮'),
639
- ('context360', cmd_context360, '符号 360 度视图(端点/函数/处理器)'),
640
- ('context', cmd_context, '上下文打包(代码+页面+字段+PRD+API,可 --role 角色裁剪)'),
641
- ('coverage', cmd_coverage, '功能×测试覆盖矩阵(标出回归盲区)'),
642
- ('feature', cmd_feature, '功能画像(端点+按钮+测试+页面)'),
643
- ('workflow', cmd_workflow, '业务流程链(query→create→audit→...)'),
644
- ('hop', cmd_hop, '多跳遍历(知识图谱核心,从一符号递归 N 跳)'),
645
- ('wiki', cmd_wiki, 'Repo Wiki 搜索(模块语义文档)'),
646
- ('fill-prototype', cmd_fill_prototype, '原型预填(真实数据填模板,输出 80% HTML)'),
647
- ('design-system', cmd_design_system, '设计系统查询(布局指纹/颜色token/组件表)'),
648
- ]
649
-
650
-
651
- def _usage():
652
- """打印用法清单。"""
653
- lines = [
654
- 'kg.py — QODER 知识图谱统一 CLI(全系列通用)',
655
- '',
656
- '用法: python .qoder/scripts/kg.py <子命令> [参数]',
657
- '',
658
- '13 个子命令:',
659
- ]
660
- for name, _, desc in COMMANDS:
661
- lines.append(' %-16s %s' % (name, desc))
662
- lines += [
663
- '',
664
- '示例:',
665
- ' kg.py search 考勤 --platform web',
666
- ' kg.py impact /asset # 改 /asset 影响谁',
667
- ' kg.py coverage # 哪些功能没测试',
668
- ' kg.py feature 资产管理 # 资产管理功能画像',
669
- ' kg.py workflow assets # 资产模块业务流程',
670
- ' kg.py hop 资产管理 --depth 3 # 从资产管理出发 3 跳遍历',
671
- ' kg.py context 车辆 --role pm # PM 视角的上下文打包',
672
- '',
673
- 'QoderWork 环境: AI 也会直接调 MCP 工具(mcp__qoder-knowledge-graph__*),功能等价。',
674
- ]
675
- return '\n'.join(lines)
676
-
677
-
678
- def main(argv=None):
679
- argv = argv if argv is not None else sys.argv[1:]
680
- if not argv or argv[0] in ('-h', '--help', 'help'):
681
- print(_usage())
682
- return 0
683
- cmd = argv[0]
684
- rest = argv[1:]
685
- # 查找子命令
686
- handler = None
687
- for name, fn, _ in COMMANDS:
688
- if name == cmd:
689
- handler = fn
690
- break
691
- if handler is None:
692
- print("未知子命令: %s" % cmd)
693
- print(_usage())
694
- return 1
695
- # 执行(全容错,绝不抛栈)
696
- try:
697
- result = handler(rest)
698
- if result:
699
- print(result)
700
- except Exception as e:
701
- print("[error] %s 执行失败: %s" % (cmd, str(e)))
702
- print("如果是索引/图谱问题,尝试: python .qoder/scripts/init_doctor.py --fix")
703
- return 1
704
- return 0
705
-
706
-
707
- if __name__ == '__main__':
708
- sys.exit(main())
1
+ # -*- coding: utf-8 -*-
2
+ # v3.0 路径自举: 引导到 common/bootstrap, 统一 sys.path 逻辑
3
+ import os as _o, sys as _s
4
+ _cp = _o.path.join(_o.path.dirname(_o.path.dirname(_o.path.abspath(__file__))), 'common')
5
+ if _cp not in _s.path: _s.path.insert(0, _cp)
6
+ from bootstrap import setup; setup()
7
+
8
+ """
9
+ kg.py — QODER 知识图谱统一 CLI 入口(全系列通用)
10
+
11
+ 把原本只在 QoderWork MCP (kg_mcp_server.py) 里的知识图谱能力,
12
+ 统一成一套命令行子命令。Qoder IDE / Quest / CLI 直接跑;QoderWork 仍可
13
+ MCP,本脚本是新增的并列通道(功能等价)。
14
+
15
+ 设计原则:
16
+ 1. 不复制逻辑 — 调底层: search_index / context_pack / graph_traverse /
17
+ kg_duckdb / repowiki / fill_prototype / gen_design_doc
18
+ 2. 全容错 — DuckDB/索引缺失优雅降级,给修复提示,绝不抛栈给用户
19
+ 3. 参数统一 13 个子命令风格一致,对标 MCP 工具名
20
+ 4. 纯文本输出 — UTF-8,无日志噪音,AI 直接读
21
+
22
+ 用法:
23
+ python .qoder/scripts/kg.py <子命令> [参数]
24
+
25
+ python .qoder/scripts/kg.py search 考勤 --platform web
26
+ python .qoder/scripts/kg.py impact /asset
27
+ python .qoder/scripts/kg.py coverage
28
+ python .qoder/scripts/kg.py feature 资产管理
29
+ python .qoder/scripts/kg.py workflow assets
30
+ python .qoder/scripts/kg.py hop 资产管理 --depth 3
31
+ """
32
+ import io
33
+ import os
34
+ import re
35
+ import sys
36
+
37
+ # UTF-8 输出(Windows 终端默认 GBK 会乱码)
38
+ try:
39
+ sys.stdout.reconfigure(encoding='utf-8', errors='replace')
40
+ except Exception:
41
+ pass
42
+
43
+ SCRIPTS_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # 子包→scripts/根
44
+ BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(SCRIPTS_DIR))) # 多一层: 子包→scripts→repo # repo root
45
+ sys.path.insert(0, SCRIPTS_DIR)
46
+ _KG_DIR = os.path.dirname(os.path.abspath(__file__))
47
+ if _KG_DIR not in sys.path: sys.path.insert(0, _KG_DIR) # 同包import
48
+
49
+
50
+ # ── 路径与索引新鲜度(与底层一致)──────────────────────────────────────
51
+ def _index_dir():
52
+ """获取 data/index 目录(延迟导入,避免顶层崩)。"""
53
+ try:
54
+ from common.paths import DATA_INDEX_DIR
55
+ return str(DATA_INDEX_DIR)
56
+ except Exception:
57
+ # 兜底: 常规布局
58
+ return os.path.join(BASE_DIR, 'data', 'index')
59
+
60
+
61
+ def _kg_db_path():
62
+ return os.path.join(_index_dir(), 'kg.duckdb')
63
+
64
+
65
+ def _has_duckdb():
66
+ """知识图谱 DuckDB 是否已构建。"""
67
+ return os.path.isfile(_kg_db_path())
68
+
69
+
70
+ def _hint_rebuild(action_cn='知识图谱'):
71
+ """DuckDB/索引缺失时的统一修复提示。"""
72
+ return (
73
+ "⚠️ %s未构建或缺失。\n"
74
+ "修复方法(任选其一):\n"
75
+ " 1. AI 说「初始化」(/wl-init) — 自动刷新索引\n"
76
+ " 2. 手动构建: python .qoder/scripts/kg_build.py\n"
77
+ " 3. 自检修复: python .qoder/scripts/init_doctor.py --fix\n"
78
+ "本次查询无法继续,但其他子命令(不依赖图谱的)仍可用。"
79
+ % action_cn
80
+ )
81
+
82
+
83
+ # ── 子命令实现 ────────────────────────────────────────────────────────
84
+
85
+ def cmd_search(args):
86
+ """搜代码: 按关键词找代码文件。"""
87
+ if not args:
88
+ return "用法: kg.py search <关键词> [--platform web|app]"
89
+ kw, plat = _split_kw_platform(args)
90
+ if not kw:
91
+ return "请提供搜索关键词"
92
+ import search_index as S
93
+ # search_keywords(query, platform=None) 自带中文扩展,不需外层 expand
94
+ return _capture(S.search_keywords, kw, plat) or "无匹配结果"
95
+
96
+
97
+ def cmd_api(args):
98
+ """搜 API 端点。"""
99
+ if not args:
100
+ return "用法: kg.py api <关键词>"
101
+ import search_index as S
102
+ return _capture(S.search_api, args[0]) or "无匹配 API"
103
+
104
+
105
+ def cmd_prd(args):
106
+ """ PRD(防重复造轮子)。"""
107
+ if not args:
108
+ return "用法: kg.py prd <关键词>"
109
+ import search_index as S
110
+ return _capture(S.search_prds, args[0]) or "无匹配 PRD"
111
+
112
+
113
+ def cmd_impact(args):
114
+ """影响分析: 改某接口影响哪些前端页面/按钮。"""
115
+ if not args:
116
+ return "用法: kg.py impact <端点或关键词> 例: /assetexport"
117
+ import search_index as S
118
+ return _capture(S.search_impact, args[0]) or "未找到匹配端点"
119
+
120
+
121
+ def cmd_context360(args):
122
+ """符号 360 度视图: 端点/函数/处理器的全部关联。"""
123
+ if not args:
124
+ return "用法: kg.py context360 <符号> 例: handleExport /system/role"
125
+ import search_index as S
126
+ return _capture(S.search_context360, args[0]) or "未找到匹配符号"
127
+
128
+
129
+ def cmd_context(args):
130
+ """上下文打包: 一次返回关键词相关的全部上下文。支持 --role 角色裁剪。"""
131
+ if not args:
132
+ return ("用法: kg.py context <关键词> [--platform web|app] "
133
+ "[--role pm|design|dev|test|admin]")
134
+ rest = list(args)
135
+ role = ''
136
+ plat = ''
137
+ # 解析 --role / --platform
138
+ rest2 = []
139
+ i = 0
140
+ while i < len(rest):
141
+ a = rest[i]
142
+ if a == '--role' and i + 1 < len(rest):
143
+ role = rest[i + 1]; i += 2; continue
144
+ if a == '--platform' and i + 1 < len(rest):
145
+ plat = rest[i + 1]; i += 2; continue
146
+ rest2.append(a); i += 1
147
+ kw = rest2[0] if rest2 else ''
148
+ if not kw:
149
+ return "请提供业务关键词"
150
+ import context_pack as C
151
+ full = _capture(C._compute_and_print, kw, plat, '')
152
+ if not full:
153
+ return "无匹配上下文"
154
+ if role:
155
+ return _filter_by_role(full, role)
156
+ return full
157
+
158
+
159
+ def cmd_prefetch(args):
160
+ """需求上下文预测 + 批量预取: 输入一句需求描述, 一次返回多词合并的上下文。
161
+
162
+ "串行试探式搜索"(查一个词不命中 → 换词再查 6+ 次)的轮次爆炸。
163
+ 支持 --platform。
164
+ """
165
+ if not args:
166
+ return ("用法: kg.py prefetch \"<需求描述>\" [--platform web|app]\n"
167
+ "例: kg.py prefetch \"保险单OCR识别标注对比图\" --platform web")
168
+ rest = list(args)
169
+ plat = ''
170
+ rest2 = []
171
+ i = 0
172
+ while i < len(rest):
173
+ a = rest[i]
174
+ if a == '--platform' and i + 1 < len(rest):
175
+ plat = rest[i + 1]; i += 2; continue
176
+ rest2.append(a); i += 1
177
+ query = ' '.join(rest2) if rest2 else ''
178
+ if not query:
179
+ return "请提供需求描述(一句话)"
180
+ import prefetch as P
181
+ primary_cn, en_words = P.predict_words(query)
182
+ if not en_words:
183
+ # 降级: 预测不出词 单词 context_pack
184
+ import context_pack as C
185
+ return _capture(C._compute_and_print, query, plat, '') or "无匹配上下文"
186
+ targets = []
187
+ if plat and plat.lower() not in ('both', '两端'):
188
+ targets = [P.PLATFORM_MAP.get(plat.lower(), plat)]
189
+ elif plat:
190
+ targets = sorted(set(P.PLATFORM_MAP.values()))
191
+ collected = P._collect(targets, en_words)
192
+ return _capture(P._print_merged, query, plat, primary_cn, en_words, collected) or "无匹配上下文"
193
+
194
+
195
+ def cmd_coverage(args):
196
+ """功能×测试覆盖矩阵: 哪些功能有测试,哪些是盲区。"""
197
+ from common.graph_traverse import CodeGraph
198
+ g = CodeGraph(_index_dir())
199
+ return g.coverage_matrix() or "无法生成覆盖矩阵(索引可能为空)"
200
+
201
+
202
+ def cmd_feature(args):
203
+ """功能画像: 一个功能的完整画像(端点+按钮+测试)。
204
+
205
+ DuckDB 优先(DuckDB 数据最全);DuckDB 缺失时回退到 CodeGraph JSON 路径。
206
+ """
207
+ if not args:
208
+ return "用法: kg.py feature <功能名> 例: 资产管理 或 asset 或 system"
209
+ feat = args[0]
210
+ # DuckDB 优先(数据最全,coverage 已验证可用)
211
+ if _has_duckdb():
212
+ try:
213
+ import duckdb
214
+ except ImportError:
215
+ return "未安装 duckdb。请运行: pip install duckdb"
216
+ con = duckdb.connect(_kg_db_path(), read_only=True)
217
+ try:
218
+ result = _feature_duckdb(con, feat)
219
+ if result:
220
+ return result
221
+ finally:
222
+ try: con.close()
223
+ except Exception: pass
224
+ # 回退: CodeGraph 的 JSON 实现(entity-registry.json 通常不存在)
225
+ from common.graph_traverse import CodeGraph
226
+ g = CodeGraph(_index_dir())
227
+ return g.feature_overview(feat) or "未找到功能 '%s'" % feat
228
+
229
+
230
+ def _feature_duckdb(con, query):
231
+ """从 DuckDB 查功能画像(feature → endpoints/buttons/tests)。
232
+
233
+ 别名解析: aliases 精确 canonical/cn 模糊 entity_id 含关键词。
234
+ """
235
+ q = (query or '').strip()
236
+ # 1) 解析为 FEATURE entity_id
237
+ target = None
238
+ resolved = con.execute(
239
+ "SELECT entity_id FROM aliases WHERE alias = ? "
240
+ "AND entity_id IN (SELECT id FROM entities WHERE type='FEATURE')",
241
+ [q.lower()]
242
+ ).fetchone()
243
+ if resolved:
244
+ target = resolved[0]
245
+ else:
246
+ # canonical / cn 模糊
247
+ row = con.execute(
248
+ "SELECT id FROM entities WHERE type='FEATURE' "
249
+ "AND (LOWER(canonical)=? OR cn=? OR LOWER(canonical) LIKE '%'||?||'%' OR cn LIKE '%'||?||'%') "
250
+ "LIMIT 1",
251
+ [q.lower(), q, q.lower(), q]
252
+ ).fetchone()
253
+ if row:
254
+ target = row[0]
255
+ if not target:
256
+ # 列出可用 feature 帮用户选
257
+ feats = con.execute(
258
+ "SELECT canonical, cn FROM entities WHERE type='FEATURE' AND cn<>'' ORDER BY cn LIMIT 30"
259
+ ).fetchall()
260
+ if not feats:
261
+ # entities 表为空 — 语义层未构建 (需要 build_entity_registry 等脚本, 非数据缺失)
262
+ total = con.execute("SELECT COUNT(*) FROM entities").fetchone()[0]
263
+ if total == 0:
264
+ return ("未找到功能 '%s'。知识图谱语义层 (entities) 为空 — "
265
+ "需要管理员构建: npx @hupan56/wlkj run build_style_index.py "
266
+ "(生成 entity-registry 后由 kg_duckdb 导入)。" % q)
267
+ cands = ', '.join('%s(%s)' % (c, cn) if cn and cn != c else c for c, cn in feats)
268
+ return "未找到功能 '%s'。可用功能: %s" % (q, cands)
269
+
270
+ # 2) feature 基本信息
271
+ info = con.execute(
272
+ "SELECT canonical, cn FROM entities WHERE id=?", [target]
273
+ ).fetchone()
274
+ canon, cn = info if info else (target, target)
275
+
276
+ # 3) 统计端点 / 按钮
277
+ endpoints = con.execute(
278
+ "SELECT to_id FROM edges WHERE from_id=? AND edge_type='has_endpoint'", [target]
279
+ ).fetchall()
280
+ buttons = con.execute(
281
+ "SELECT to_id FROM edges WHERE from_id=? AND edge_type='has_button'", [target]
282
+ ).fetchall()
283
+
284
+ # 4) 关联测试: feature 的端点 -(handled_by)-> controller -(tested_by)-> test
285
+ try:
286
+ tests = con.execute("""
287
+ SELECT DISTINCT tb.to_id FROM edges he
288
+ JOIN edges tb ON tb.from_id = he.to_id AND tb.edge_type='tested_by'
289
+ WHERE he.from_id=? AND he.edge_type='handled_by'
290
+ """, [target]).fetchall()
291
+ except Exception:
292
+ tests = []
293
+
294
+ ep_ids = [e[0] for e in endpoints]
295
+ btn_ids = [b[0] for b in buttons]
296
+ test_ids = [t[0] for t in tests]
297
+
298
+ # 5) 组装画像
299
+ out = ['## 功能画像: %s (%s)' % (cn, canon)]
300
+ out.append(' 端点 %d 个 | 按钮操作 %d 个 | 关联测试 %d 个' % (
301
+ len(ep_ids), len(btn_ids), len(test_ids)))
302
+ out.append('')
303
+
304
+ out.append('### 端点 (%d)' % len(ep_ids))
305
+ for eid in ep_ids[:10]:
306
+ out.append(' %s' % eid)
307
+ if len(ep_ids) > 10:
308
+ out.append(' ... 还有 %d 个' % (len(ep_ids) - 10))
309
+ out.append('')
310
+
311
+ out.append('### 按钮操作 (%d)' % len(btn_ids))
312
+ # 提取按钮的操作动词(handleAdd/handleDel 等)
313
+ verbs = {}
314
+ for bid in btn_ids:
315
+ # bid 形如 B:path/to/file.vue:handleAdd
316
+ if ':' in bid:
317
+ handler = bid.rsplit(':', 1)[-1]
318
+ else:
319
+ handler = bid
320
+ verbs[handler] = verbs.get(handler, 0) + 1
321
+ top_verbs = sorted(verbs.items(), key=lambda x: -x[1])[:15]
322
+ out.append(' 操作分布: %s' % ', '.join('%s×%d' % (v, n) for v, n in top_verbs))
323
+ out.append('')
324
+
325
+ out.append('### 关联测试 (%d)' % len(test_ids))
326
+ shown = 0
327
+ for tid in test_ids:
328
+ if shown >= 6:
329
+ out.append(' ... 还有 %d 个' % (len(test_ids) - 6))
330
+ break
331
+ # tid 形如 T:ClassName.method,查 tests 表拿断言
332
+ if tid.startswith('T:'):
333
+ ref = tid[2:]
334
+ cls = method = ''
335
+ if '.' in ref:
336
+ cls, method = ref.rsplit('.', 1)
337
+ trow = con.execute(
338
+ "SELECT display_name, assertions FROM tests WHERE class=? AND method=? LIMIT 1",
339
+ [cls, method]
340
+ ).fetchone()
341
+ if trow:
342
+ dn, asserts = trow
343
+ import json as _json
344
+ try:
345
+ a_list = _json.loads(asserts) if isinstance(asserts, str) else asserts
346
+ a_str = ', '.join(a_list[:3]) if a_list else ''
347
+ except Exception:
348
+ a_str = ''
349
+ out.append(' %s — %s%s' % (ref, dn or '', (' [断言: %s]' % a_str) if a_str else ''))
350
+ else:
351
+ out.append(' %s' % ref)
352
+ else:
353
+ out.append(' %s' % tid)
354
+ shown += 1
355
+
356
+ if len(test_ids) == 0:
357
+ out.append(' ⚠️ 该功能无关联测试,是回归测试盲区')
358
+
359
+ return '\n'.join(out)
360
+
361
+
362
+ def cmd_workflow(args):
363
+ """业务流程链: 一个模块的完整操作链(query→create→audit→...)。
364
+
365
+ 优先匹配英文模块名(assets/cost/hr/...),中文输入返回候选。
366
+ 数据源: DuckDB workflows 表。
367
+ """
368
+ if not args:
369
+ return ("用法: kg.py workflow <模块名> 例: assets / cost / hr\n"
370
+ "可选模块: assets/cost/hr/maintenance/operation/"
371
+ "qualityAndSafety/veh/settings/dashboard/flow/systems/monitor/quasafe")
372
+ module = args[0]
373
+ if not _has_duckdb():
374
+ return _hint_rebuild('业务流程数据(DuckDB)')
375
+ try:
376
+ import duckdb
377
+ except ImportError:
378
+ return ("未安装 duckdb。请运行: pip install duckdb")
379
+ con = duckdb.connect(_kg_db_path(), read_only=True)
380
+ try:
381
+ return _workflow_query(con, module)
382
+ finally:
383
+ try: con.close()
384
+ except Exception: pass
385
+
386
+
387
+ def _workflow_query(con, q):
388
+ """workflows 表查询 — v3.0 f3: 转发到 common.kg_capabilities (单一真源, CLI/MCP共用)。"""
389
+ from common.kg_capabilities import workflow_query
390
+ return workflow_query(con, q)
391
+
392
+
393
+ def cmd_hop(args):
394
+ """多跳遍历: 从一个符号出发,递归 N 跳找全部可达实体。
395
+
396
+ 知识图谱核心能力。支持别名解析(传中文/端点/函数名自动转 entity_id)。
397
+ """
398
+ if not args:
399
+ return "用法: kg.py hop <符号> [--depth 1-5] 例: 资产管理 或 /asset/list"
400
+ symbol, depth = _split_symbol_depth(args)
401
+ if not _has_duckdb():
402
+ return _hint_rebuild('知识图谱(DuckDB)')
403
+ try:
404
+ import duckdb
405
+ except ImportError:
406
+ return "未安装 duckdb。请运行: pip install duckdb"
407
+ import kg_duckdb as K
408
+ con = duckdb.connect(_kg_db_path(), read_only=True)
409
+ try:
410
+ # v3.0 f3: 核心查询转发到 common.kg_capabilities.hop_query (CLI/MCP共用)
411
+ from common.kg_capabilities import hop_query
412
+ return hop_query(con, symbol, depth, multi_hop_fn=K.multi_hop)
413
+ finally:
414
+ try: con.close()
415
+ except Exception: pass
416
+
417
+
418
+ def cmd_wiki(args):
419
+ """Repo Wiki 搜索 — v3.0 f3: 转发到 common.kg_capabilities.wiki_query (CLI/MCP共用)。"""
420
+ if not args:
421
+ return "用法: kg.py wiki <关键词>"
422
+ from common.kg_capabilities import wiki_query
423
+ result = wiki_query(args[0])
424
+ # CLI 额外提示
425
+ if 'matching' in result:
426
+ result += '\n\n 提示: 读 wiki 文档(带行号引用)比读源码更省 token'
427
+ return result
428
+
429
+
430
+ def cmd_fill_prototype(args):
431
+ """原型预填: 用真实代码数据自动填模板,输出 80% 完成的 HTML 草稿。"""
432
+ if not args:
433
+ return "用法: kg.py fill-prototype <关键词> [--platform web|app]"
434
+ kw, plat = _split_kw_platform(args)
435
+ plat = plat or 'web'
436
+ import subprocess as _sp
437
+ script = os.path.join(SCRIPTS_DIR, 'fill_prototype.py')
438
+ if not os.path.isfile(script):
439
+ return "fill_prototype.py 不存在"
440
+ try:
441
+ r = _sp.run([sys.executable, script, kw, '--platform', plat],
442
+ capture_output=True, text=True, encoding='utf-8',
443
+ errors='replace', timeout=30, cwd=SCRIPTS_DIR)
444
+ return r.stdout or "fill_prototype 无输出(可能索引缺失)"
445
+ except Exception as e:
446
+ return "[error] fill_prototype: %s" % str(e)
447
+
448
+
449
+ def cmd_design_system(args):
450
+ """设计系统查询: 返回项目完整设计规范(布局指纹/颜色token/组件表/图标)。"""
451
+ plat = ''
452
+ if args and args[0] in ('--platform', '-p') and len(args) > 1:
453
+ plat = args[1]
454
+ import subprocess as _sp
455
+ design_md = os.path.join(BASE_DIR, 'data', 'design', 'DESIGN.md')
456
+ if not os.path.isfile(design_md):
457
+ gen_script = os.path.join(SCRIPTS_DIR, 'gen_design_doc.py')
458
+ if os.path.isfile(gen_script):
459
+ _sp.run([sys.executable, gen_script], capture_output=True, timeout=30)
460
+ if os.path.isfile(design_md):
461
+ content = open(design_md, encoding='utf-8').read()
462
+ if plat:
463
+ lines = content.split('\n')
464
+ result = []
465
+ in_section = False
466
+ for line in lines:
467
+ if line.startswith('### ') and plat.lower() in line.lower():
468
+ in_section = True
469
+ elif line.startswith('### ') and in_section:
470
+ break
471
+ if in_section:
472
+ result.append(line)
473
+ return '\n'.join(result) if result else content
474
+ return content
475
+ return "DESIGN.md 不存在且生成失败。请手动跑: python .qoder/scripts/gen_design_doc.py"
476
+
477
+
478
+ # ── 工具函数 ──────────────────────────────────────────────────────────
479
+
480
+ def _capture(func, *args, **kwargs):
481
+ """运行函数,捕获其 stdout 输出(底层脚本用 print 输出)。"""
482
+ old = sys.stdout
483
+ buf = io.StringIO()
484
+ sys.stdout = buf
485
+ try:
486
+ func(*args, **kwargs)
487
+ except Exception as e:
488
+ buf.write('\n[error] %s' % str(e))
489
+ finally:
490
+ sys.stdout = old
491
+ return buf.getvalue().strip()
492
+
493
+
494
+ def _split_kw_platform(args):
495
+ """从参数里拆出 keyword 和 --platform 值。"""
496
+ rest = list(args)
497
+ plat = ''
498
+ out = []
499
+ i = 0
500
+ while i < len(rest):
501
+ a = rest[i]
502
+ if a == '--platform' and i + 1 < len(rest):
503
+ plat = rest[i + 1]; i += 2; continue
504
+ out.append(a); i += 1
505
+ kw = out[0] if out else ''
506
+ return kw, plat
507
+
508
+
509
+ def _split_symbol_depth(args):
510
+ """从参数里拆出 symbol --depth 值(默认 3)。"""
511
+ rest = list(args)
512
+ depth = 3
513
+ out = []
514
+ i = 0
515
+ while i < len(rest):
516
+ a = rest[i]
517
+ if a == '--depth' and i + 1 < len(rest):
518
+ try: depth = int(rest[i + 1])
519
+ except (ValueError, TypeError): depth = 3
520
+ i += 2; continue
521
+ out.append(a); i += 1
522
+ return (out[0] if out else ''), max(1, min(5, depth))
523
+
524
+
525
+ # Role-based section filtering for context_pack output
526
+ # v3.0: 单一真源移到 common/roles.py, 消除与 kg_mcp_server.py 的逐字重复
527
+ try:
528
+ from common.roles import ROLE_SECTIONS, filter_by_role as _filter_by_role_imported
529
+ _filter_by_role = _filter_by_role_imported # 保持旧调用名 _filter_by_role 兼容
530
+ except Exception:
531
+ # 回退: 内联定义 (common 不可用时, common/roles.py 保持一致)
532
+ ROLE_SECTIONS = {
533
+ 'pm': {'1', '2', '2b', '3', '4', '5', '7'},
534
+ 'design': {'2', '2b', '3', '6'},
535
+ 'dev': {'1', '3', '5', '7'},
536
+ 'test': {'1', '4', '5'},
537
+ 'admin': {'4', '5'},
538
+ }
539
+
540
+ def _filter_by_role(text, role):
541
+ visible = ROLE_SECTIONS.get((role or '').lower())
542
+ if not visible:
543
+ return text
544
+ sections = re.split(r'(\n## \d+[ab]?\.)', text)
545
+ result = sections[0]
546
+ i = 1
547
+ while i < len(sections):
548
+ header = sections[i]
549
+ num_m = re.match(r'\n## (\d+[ab]?)\.', header)
550
+ if num_m and num_m.group(1) in visible:
551
+ result += header
552
+ if i + 1 < len(sections):
553
+ result += sections[i + 1]
554
+ i += 2
555
+ else:
556
+ i += 2
557
+ return result.strip()
558
+
559
+
560
+ # ── 子命令注册表 ──────────────────────────────────────────────────────
561
+ # (子命令名, 处理函数, 中文说明)
562
+ COMMANDS = [
563
+ ('search', cmd_search, '搜代码: 按关键词找代码文件'),
564
+ ('api', cmd_api, '搜 API 端点'),
565
+ ('prd', cmd_prd, '搜 PRD(防重复造轮子)'),
566
+ ('impact', cmd_impact, '影响分析: 改某接口影响哪些页面/按钮'),
567
+ ('context360', cmd_context360, '符号 360 度视图(端点/函数/处理器)'),
568
+ ('context', cmd_context, '上下文打包(代码+页面+字段+PRD+API,可 --role 角色裁剪)'),
569
+ ('prefetch', cmd_prefetch, '需求上下文预测+批量预取(一句需求→多词合并,治串行搜索)'),
570
+ ('coverage', cmd_coverage, '功能×测试覆盖矩阵(标出回归盲区)'),
571
+ ('feature', cmd_feature, '功能画像(端点+按钮+测试+页面)'),
572
+ ('workflow', cmd_workflow, '业务流程链(query→create→audit→...)'),
573
+ ('hop', cmd_hop, '多跳遍历(知识图谱核心,从一符号递归 N 跳)'),
574
+ ('wiki', cmd_wiki, 'Repo Wiki 搜索(模块语义文档)'),
575
+ ('fill-prototype', cmd_fill_prototype, '原型预填(真实数据填模板,输出 80% HTML)'),
576
+ ('design-system', cmd_design_system, '设计系统查询(布局指纹/颜色token/组件表)'),
577
+ ]
578
+
579
+
580
+ def _usage():
581
+ """打印用法清单。"""
582
+ lines = [
583
+ 'kg.py — QODER 知识图谱统一 CLI(全系列通用)',
584
+ '',
585
+ '用法: python .qoder/scripts/kg.py <子命令> [参数]',
586
+ '',
587
+ '14 个子命令:',
588
+ ]
589
+ for name, _, desc in COMMANDS:
590
+ lines.append(' %-16s %s' % (name, desc))
591
+ lines += [
592
+ '',
593
+ '示例:',
594
+ ' kg.py search 考勤 --platform web',
595
+ ' kg.py prefetch "保险单OCR识别" --platform web # 写PRD前先批量预取',
596
+ ' kg.py impact /asset # 改 /asset 影响谁',
597
+ ' kg.py coverage # 哪些功能没测试',
598
+ ' kg.py feature 资产管理 # 资产管理功能画像',
599
+ ' kg.py workflow assets # 资产模块业务流程',
600
+ ' kg.py hop 资产管理 --depth 3 # 从资产管理出发 3 跳遍历',
601
+ ' kg.py context 车辆 --role pm # PM 视角的上下文打包',
602
+ '',
603
+ 'QoderWork 环境: AI 也会直接调 MCP 工具(mcp__qoder-knowledge-graph__*),功能等价。',
604
+ ]
605
+ return '\n'.join(lines)
606
+
607
+
608
+ def main(argv=None):
609
+ argv = argv if argv is not None else sys.argv[1:]
610
+ if not argv or argv[0] in ('-h', '--help', 'help'):
611
+ print(_usage())
612
+ return 0
613
+ cmd = argv[0]
614
+ rest = argv[1:]
615
+ # 查找子命令
616
+ handler = None
617
+ for name, fn, _ in COMMANDS:
618
+ if name == cmd:
619
+ handler = fn
620
+ break
621
+ if handler is None:
622
+ print("未知子命令: %s" % cmd)
623
+ print(_usage())
624
+ return 1
625
+ # 执行(全容错,绝不抛栈)
626
+ try:
627
+ result = handler(rest)
628
+ if result:
629
+ print(result)
630
+ except Exception as e:
631
+ print("[error] %s 执行失败: %s" % (cmd, str(e)))
632
+ print("如果是索引/图谱问题,尝试: python .qoder/scripts/init_doctor.py --fix")
633
+ return 1
634
+ return 0
635
+
636
+
637
+ if __name__ == '__main__':
638
+ sys.exit(main())