@hupan56/wlkj 2.7.12 → 3.1.2

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 (253) hide show
  1. package/bin/cli.js +1435 -1107
  2. package/package.json +29 -29
  3. package/templates/qoder/agents/insight-planning.md +1 -1
  4. package/templates/qoder/agents/insight-research.md +28 -15
  5. package/templates/qoder/commands/optional/wl-insight.md +161 -161
  6. package/templates/qoder/commands/optional/wl-report.md +20 -39
  7. package/templates/qoder/commands/optional/wl-status.md +4 -4
  8. package/templates/qoder/commands/wl-code.md +2 -2
  9. package/templates/qoder/commands/wl-design.md +6 -6
  10. package/templates/qoder/commands/wl-init.md +3 -3
  11. package/templates/qoder/commands/wl-prd.md +26 -13
  12. package/templates/qoder/commands/wl-req.md +43 -0
  13. package/templates/qoder/commands/wl-search.md +78 -47
  14. package/templates/qoder/commands/wl-task.md +343 -40
  15. package/templates/qoder/commands/wl-test.md +43 -17
  16. package/templates/qoder/config.yaml +52 -14
  17. package/templates/qoder/hooks/post-tool-use.py +181 -0
  18. package/templates/qoder/hooks/session-start.py +94 -23
  19. package/templates/qoder/hooks/stop-eval.py +207 -0
  20. package/templates/qoder/hooks/user-prompt-submit.py +139 -0
  21. package/templates/qoder/rules/wl-pipeline.md +85 -49
  22. package/templates/qoder/scripts/README.md +102 -0
  23. package/templates/qoder/scripts/capability/__init__.py +26 -0
  24. package/templates/qoder/scripts/capability/__main__.py +72 -0
  25. package/templates/qoder/scripts/capability/adapters/__init__.py +29 -0
  26. package/templates/qoder/scripts/capability/adapters/cli.py +316 -0
  27. package/templates/qoder/scripts/capability/adapters/mcp.py +334 -0
  28. package/templates/qoder/scripts/capability/adapters/qw.py +271 -0
  29. package/templates/qoder/scripts/capability/caps/__init__.py +27 -0
  30. package/templates/qoder/scripts/capability/caps/context.py +36 -0
  31. package/templates/qoder/scripts/capability/caps/cron.py +50 -0
  32. package/templates/qoder/scripts/capability/caps/identity.py +43 -0
  33. package/templates/qoder/scripts/capability/caps/memory.py +71 -0
  34. package/templates/qoder/scripts/capability/caps/notify.py +41 -0
  35. package/templates/qoder/scripts/capability/caps/present.py +48 -0
  36. package/templates/qoder/scripts/capability/caps/repo.py +31 -0
  37. package/templates/qoder/scripts/capability/caps/sandbox.py +43 -0
  38. package/templates/qoder/scripts/capability/chain.py +92 -0
  39. package/templates/qoder/scripts/capability/memory_chain.py +41 -0
  40. package/templates/qoder/scripts/capability/registry.py +246 -0
  41. package/templates/qoder/scripts/capability/registry_mcp.py +250 -0
  42. package/templates/qoder/scripts/capability/smoke_test.py +211 -0
  43. package/templates/qoder/scripts/capability/smoke_test_report.json +94 -0
  44. package/templates/qoder/scripts/deployment/setup/__init__.py +11 -0
  45. package/templates/qoder/scripts/deployment/setup/carriers.py +669 -0
  46. package/templates/qoder/scripts/deployment/setup/carriers_verify.py +148 -0
  47. package/templates/qoder/scripts/{init_doctor.py → deployment/setup/init_doctor.py} +100 -39
  48. package/templates/qoder/scripts/{install_qoderwork.py → deployment/setup/install_qoderwork.py} +48 -30
  49. package/templates/qoder/scripts/{platform_doctor.py → deployment/setup/platform_doctor.py} +271 -259
  50. package/templates/qoder/scripts/{repo_root.py → deployment/setup/repo_root.py} +9 -2
  51. package/templates/qoder/scripts/{setup.py → deployment/setup/setup.py} +143 -14
  52. package/templates/qoder/scripts/{setup_lanhu.py → deployment/setup/setup_lanhu.py} +979 -963
  53. package/templates/qoder/scripts/domain/design/__init__.py +0 -0
  54. package/templates/qoder/scripts/{fill_prototype.py → domain/design/fill_prototype.py} +17 -5
  55. package/templates/qoder/scripts/{gen_design_doc.py → domain/design/gen_design_doc.py} +406 -394
  56. package/templates/qoder/scripts/domain/kg/__init__.py +11 -0
  57. package/templates/qoder/scripts/domain/kg/build/__init__.py +0 -0
  58. package/templates/qoder/scripts/domain/kg/build/build_entity_registry.py +201 -0
  59. package/templates/qoder/scripts/domain/kg/build/build_relations.py +132 -0
  60. package/templates/qoder/scripts/{build_style_index.py → domain/kg/build/build_style_index.py} +50 -15
  61. package/templates/qoder/scripts/domain/kg/build/build_workflows.py +149 -0
  62. package/templates/qoder/scripts/{kg_build.py → domain/kg/build/kg_build.py} +685 -612
  63. package/templates/qoder/scripts/{kg_build_db.py → domain/kg/build/kg_build_db.py} +338 -327
  64. package/templates/qoder/scripts/{kg_incremental.py → domain/kg/build/kg_incremental.py} +425 -393
  65. package/templates/qoder/scripts/{learn_aggregate.py → domain/kg/build/learn_aggregate.py} +212 -201
  66. package/templates/qoder/scripts/domain/kg/extract/__init__.py +0 -0
  67. package/templates/qoder/scripts/{common → domain/kg/extract}/extract.py +430 -419
  68. package/templates/qoder/scripts/domain/kg/extract/test_extract.py +115 -0
  69. package/templates/qoder/scripts/{common → domain/kg/extract}/ts_extract.py +614 -536
  70. package/templates/qoder/scripts/domain/kg/graph/__init__.py +0 -0
  71. package/templates/qoder/scripts/{kg_link_db.py → domain/kg/graph/kg_link_db.py} +235 -224
  72. package/templates/qoder/scripts/domain/kg/graph/kg_semantic.py +683 -0
  73. package/templates/qoder/scripts/{kg.py → domain/kg/kg.py} +871 -708
  74. package/templates/qoder/scripts/domain/kg/kg_capabilities.py +182 -0
  75. package/templates/qoder/scripts/domain/kg/search/__init__.py +0 -0
  76. package/templates/qoder/scripts/{context_pack.py → domain/kg/search/context_pack.py} +45 -17
  77. package/templates/qoder/scripts/{enrich_prompt.py → domain/kg/search/enrich_prompt.py} +238 -226
  78. package/templates/qoder/scripts/domain/kg/search/prefetch.py +384 -0
  79. package/templates/qoder/scripts/{common → domain/kg/search}/search_engine.py +207 -205
  80. package/templates/qoder/scripts/{search_index.py → domain/kg/search/search_index.py} +210 -88
  81. package/templates/qoder/scripts/domain/kg/server/__init__.py +0 -0
  82. package/templates/qoder/scripts/domain/kg/server/kgd.py +549 -0
  83. package/templates/qoder/scripts/domain/kg/server/perf_bench.py +197 -0
  84. package/templates/qoder/scripts/domain/kg/storage/__init__.py +0 -0
  85. package/templates/qoder/scripts/{kg_duckdb.py → domain/kg/storage/kg_duckdb.py} +70 -45
  86. package/templates/qoder/scripts/domain/learning/__init__.py +0 -0
  87. package/templates/qoder/scripts/{learn.py → domain/learning/learn.py} +157 -146
  88. package/templates/qoder/scripts/domain/report/__init__.py +11 -0
  89. package/templates/qoder/scripts/{add_session.py → domain/report/add_session.py} +256 -244
  90. package/templates/qoder/scripts/{export.py → domain/report/export.py} +72 -4
  91. package/templates/qoder/scripts/{report.py → domain/report/report.py} +292 -281
  92. package/templates/qoder/scripts/domain/report/report_snapshot.py +261 -0
  93. package/templates/qoder/scripts/domain/report/role.py +39 -0
  94. package/templates/qoder/scripts/{status.py → domain/report/status.py} +737 -628
  95. package/templates/qoder/scripts/domain/report/status_snapshot.py +191 -0
  96. package/templates/qoder/scripts/domain/requirement/__init__.py +0 -0
  97. package/templates/qoder/scripts/{archive_prd.py → domain/requirement/archive_prd.py} +389 -377
  98. package/templates/qoder/scripts/domain/requirement/req.py +228 -0
  99. package/templates/qoder/scripts/domain/task/__init__.py +11 -0
  100. package/templates/qoder/scripts/{git_sync.py → domain/task/git_sync.py} +90 -50
  101. package/templates/qoder/scripts/{syncgate.py → domain/task/syncgate.py} +18 -7
  102. package/templates/qoder/scripts/domain/task/task.py +229 -0
  103. package/templates/qoder/scripts/domain/task/task_lifecycle.py +606 -0
  104. package/templates/qoder/scripts/domain/task/task_query.py +162 -0
  105. package/templates/qoder/scripts/domain/task/task_relations.py +425 -0
  106. package/templates/qoder/scripts/{team_sync.py → domain/task/team_sync.py} +101 -23
  107. package/templates/qoder/scripts/domain/task/zentao_sync.py +370 -0
  108. package/templates/qoder/scripts/foundation/__init__.py +0 -0
  109. package/templates/qoder/scripts/foundation/bootstrap.py +145 -0
  110. package/templates/qoder/scripts/foundation/core/__init__.py +0 -0
  111. package/templates/qoder/scripts/foundation/core/cmd_registry.py +112 -0
  112. package/templates/qoder/scripts/foundation/core/config.py +187 -0
  113. package/templates/qoder/scripts/{common → foundation/core}/paths.py +706 -400
  114. package/templates/qoder/scripts/foundation/data/__init__.py +0 -0
  115. package/templates/qoder/scripts/foundation/data/contract.py +317 -0
  116. package/templates/qoder/scripts/{common → foundation/data}/events.py +2 -2
  117. package/templates/qoder/scripts/foundation/data/result.py +223 -0
  118. package/templates/qoder/scripts/{common → foundation/data}/task_utils.py +427 -392
  119. package/templates/qoder/scripts/foundation/identity/__init__.py +0 -0
  120. package/templates/qoder/scripts/foundation/identity/check_publish.py +103 -0
  121. package/templates/qoder/scripts/{common → foundation/identity}/developer.py +239 -238
  122. package/templates/qoder/scripts/foundation/identity/guard.py +159 -0
  123. package/templates/qoder/scripts/{common → foundation/identity}/identity.py +132 -9
  124. package/templates/qoder/scripts/foundation/identity/roles.py +60 -0
  125. package/templates/qoder/scripts/foundation/integrations/__init__.py +0 -0
  126. package/templates/qoder/scripts/{common → foundation/integrations}/active_task.py +30 -15
  127. package/templates/qoder/scripts/{common → foundation/integrations}/feishu.py +11 -10
  128. package/templates/qoder/scripts/{common → foundation/integrations}/terms.py +3 -3
  129. package/templates/qoder/scripts/foundation/integrations/zentao_client.py +220 -0
  130. package/templates/qoder/scripts/foundation/io/__init__.py +0 -0
  131. package/templates/qoder/scripts/{common → foundation/io}/filelock.py +1 -1
  132. package/templates/qoder/scripts/{common → foundation/io}/reqid.py +1 -1
  133. package/templates/qoder/scripts/foundation/protocol/__init__.py +0 -0
  134. package/templates/qoder/scripts/foundation/protocol/duckdb_conn.py +39 -0
  135. package/templates/qoder/scripts/foundation/protocol/mcp_base.py +268 -0
  136. package/templates/qoder/scripts/foundation/utils/__init__.py +0 -0
  137. package/templates/qoder/scripts/{common → foundation/utils}/eval_api.py +12 -1
  138. package/templates/qoder/scripts/{common → foundation/utils}/pip_install.py +23 -6
  139. package/templates/qoder/scripts/{common → foundation/utils}/platform_guard.py +1 -1
  140. package/templates/qoder/scripts/orchestration/__init__.py +0 -0
  141. package/templates/qoder/scripts/orchestration/wlkj.py +185 -0
  142. package/templates/qoder/scripts/protocol/__init__.py +0 -0
  143. package/templates/qoder/scripts/protocol/browser/README.md +207 -0
  144. package/templates/qoder/scripts/protocol/browser/SKILL.md +265 -0
  145. package/templates/qoder/scripts/protocol/browser/config.env +9 -0
  146. package/templates/qoder/scripts/protocol/browser/references/cdp-api.md +110 -0
  147. package/templates/qoder/scripts/protocol/browser/references/migration-2.5.3.md +72 -0
  148. package/templates/qoder/scripts/protocol/browser/references/site-patterns/.gitkeep +0 -0
  149. package/templates/qoder/scripts/protocol/browser/scripts/browser-discovery.mjs +138 -0
  150. package/templates/qoder/scripts/protocol/browser/scripts/cdp-proxy.mjs +672 -0
  151. package/templates/qoder/scripts/protocol/browser/scripts/check-deps.mjs +206 -0
  152. package/templates/qoder/scripts/protocol/browser/scripts/find-url.mjs +253 -0
  153. package/templates/qoder/scripts/protocol/browser/scripts/match-site.mjs +46 -0
  154. package/templates/qoder/scripts/protocol/browser/templates/config.env.template +9 -0
  155. package/templates/qoder/scripts/protocol/mcp/__init__.py +11 -0
  156. package/templates/qoder/scripts/protocol/mcp/kg_mcp_server.py +478 -0
  157. package/templates/qoder/scripts/{lanhu_stdio_wrapper.py → protocol/mcp/lanhu_stdio_wrapper.py} +131 -119
  158. package/templates/qoder/scripts/protocol/mcp/mcp_doctor.py +548 -0
  159. package/templates/qoder/scripts/protocol/mcp/mcp_launcher.py +600 -0
  160. package/templates/qoder/scripts/protocol/mcp/mysql_mcp_server.py +461 -0
  161. package/templates/qoder/scripts/protocol/mcp/zentao_mcp_server.py +1920 -0
  162. package/templates/qoder/scripts/protocol/transports/__init__.py +56 -0
  163. package/templates/qoder/scripts/protocol/transports/base.py +87 -0
  164. package/templates/qoder/scripts/protocol/transports/cli.py +132 -0
  165. package/templates/qoder/scripts/protocol/transports/http.py +141 -0
  166. package/templates/qoder/scripts/protocol/transports/stdio.py +293 -0
  167. package/templates/qoder/scripts/run_weekly_update.bat +27 -18
  168. package/templates/qoder/scripts/run_weekly_update.sh +22 -13
  169. package/templates/qoder/scripts/validation/__init__.py +0 -0
  170. package/templates/qoder/scripts/validation/eval/__init__.py +0 -0
  171. package/templates/qoder/scripts/validation/eval/perf-report-2026-06-24.md +139 -0
  172. package/templates/qoder/scripts/validation/eval/qwork_harness.py +617 -0
  173. package/templates/qoder/scripts/validation/eval/report-commands.md +220 -0
  174. package/templates/qoder/scripts/validation/eval/transcript_timing.py +386 -0
  175. package/templates/qoder/scripts/validation/metrics/__init__.py +0 -0
  176. package/templates/qoder/scripts/{eval_prd.py → validation/metrics/eval_prd.py} +84 -15
  177. package/templates/qoder/scripts/validation/metrics/usability_score.py +750 -0
  178. package/templates/qoder/scripts/validation/test/__init__.py +11 -0
  179. package/templates/qoder/scripts/validation/test/assertion_gen.py +551 -0
  180. package/templates/qoder/scripts/{autotest.py → validation/test/autotest.py} +1234 -1751
  181. package/templates/qoder/scripts/validation/test/autotest_auth.py +109 -0
  182. package/templates/qoder/scripts/{autotest_batch.py → validation/test/autotest_batch.py} +255 -224
  183. package/templates/qoder/scripts/validation/test/autotest_data.py +680 -0
  184. package/templates/qoder/scripts/validation/test/autotest_opencli.py +646 -0
  185. package/templates/qoder/scripts/{autotest_run.py → validation/test/autotest_run.py} +323 -297
  186. package/templates/qoder/scripts/validation/test/autotest_webaccess.py +571 -0
  187. package/templates/qoder/scripts/{benchmark.py → validation/test/benchmark.py} +17 -5
  188. package/templates/qoder/scripts/{kg_auto_login.py → validation/test/kg_auto_login.py} +208 -196
  189. package/templates/qoder/scripts/{kg_test_runner.py → validation/test/kg_test_runner.py} +13 -2
  190. package/templates/qoder/scripts/{page_probe.py → validation/test/page_probe.py} +470 -459
  191. package/templates/qoder/scripts/validation/test/smoke_all_commands.py +170 -0
  192. package/templates/qoder/settings.json +23 -2
  193. package/templates/qoder/skills/design-import/SKILL.md +235 -226
  194. package/templates/qoder/skills/design-review/SKILL.md +91 -82
  195. package/templates/qoder/skills/prd-generator/SKILL.md +38 -24
  196. package/templates/qoder/skills/prd-review/SKILL.md +8 -6
  197. package/templates/qoder/skills/prototype-generator/SKILL.md +296 -247
  198. package/templates/qoder/skills/spec-coder/SKILL.md +24 -15
  199. package/templates/qoder/skills/spec-generator/SKILL.md +24 -12
  200. package/templates/qoder/skills/test-generator/SKILL.md +9 -7
  201. package/templates/qoder/skills/wl-code/SKILL.md +25 -13
  202. package/templates/qoder/skills/wl-commit/SKILL.md +10 -9
  203. package/templates/qoder/skills/wl-design/SKILL.md +7 -5
  204. package/templates/qoder/skills/wl-init/SKILL.md +8 -8
  205. package/templates/qoder/skills/wl-insight/SKILL.md +25 -12
  206. package/templates/qoder/skills/wl-prd-full/SKILL.md +59 -8
  207. package/templates/qoder/skills/wl-prd-quick/SKILL.md +7 -7
  208. package/templates/qoder/skills/wl-prd-review/SKILL.md +18 -6
  209. package/templates/qoder/skills/wl-report/SKILL.md +27 -25
  210. package/templates/qoder/skills/wl-search/SKILL.md +151 -80
  211. package/templates/qoder/skills/wl-spec/SKILL.md +9 -7
  212. package/templates/qoder/skills/wl-status/SKILL.md +50 -18
  213. package/templates/qoder/skills/wl-task/SKILL.md +94 -12
  214. package/templates/qoder/skills/wl-test/SKILL.md +154 -45
  215. package/templates/qoder/templates/prd-full-template.md +7 -0
  216. package/templates/root/AGENTS.md +275 -237
  217. package/templates/root/requirements.txt +3 -0
  218. package/templates/root//344/275/277/347/224/250/350/257/264/346/230/216.md +3 -3
  219. package/templates/root//346/226/260/346/211/213/346/214/207/345/215/227.md +2 -2
  220. package/templates/qoder/hooks/inject-workflow-state.py +0 -169
  221. package/templates/qoder/scripts/__pycache__/check_mcp_launch.cpython-39.pyc +0 -0
  222. package/templates/qoder/scripts/__pycache__/install_qoderwork.cpython-39.pyc +0 -0
  223. package/templates/qoder/scripts/__pycache__/mcp_launcher.cpython-39.pyc +0 -0
  224. package/templates/qoder/scripts/__pycache__/platform_doctor.cpython-39.pyc +0 -0
  225. package/templates/qoder/scripts/check_carriers.py +0 -238
  226. package/templates/qoder/scripts/check_mcp.py +0 -298
  227. package/templates/qoder/scripts/check_mcp_launch.py +0 -183
  228. package/templates/qoder/scripts/check_qoderwork_consistency.py +0 -166
  229. package/templates/qoder/scripts/collect_prds.py +0 -31
  230. package/templates/qoder/scripts/common/mentions.py +0 -134
  231. package/templates/qoder/scripts/common/utf8.py +0 -38
  232. package/templates/qoder/scripts/extract_api_params.py +0 -246
  233. package/templates/qoder/scripts/extract_routes.py +0 -54
  234. package/templates/qoder/scripts/extract_routes_tree.py +0 -78
  235. package/templates/qoder/scripts/handoff.py +0 -22
  236. package/templates/qoder/scripts/init_developer.py +0 -76
  237. package/templates/qoder/scripts/kg_mcp_server.py +0 -801
  238. package/templates/qoder/scripts/kg_semantic.py +0 -150
  239. package/templates/qoder/scripts/mcp_launcher.py +0 -414
  240. package/templates/qoder/scripts/mysql_mcp_server.py +0 -396
  241. package/templates/qoder/scripts/parse_prds.py +0 -33
  242. package/templates/qoder/scripts/role.py +0 -51
  243. package/templates/qoder/scripts/sync_carriers.py +0 -259
  244. package/templates/qoder/scripts/task.py +0 -1261
  245. package/templates/qoder/scripts/workspace_init.py +0 -102
  246. package/templates/qoder/scripts/zentao_mcp_server.py +0 -424
  247. package/templates/qoder/skills/prompt-enrich/SKILL.md +0 -90
  248. /package/templates/qoder/scripts/{common → deployment}/__init__.py +0 -0
  249. /package/templates/qoder/{skills/prototype-generator/SKILL.md.zcode-79180-2af4721f-f9a6-412c-88db-c0af680d211b.tmp → scripts/domain/__init__.py} +0 -0
  250. /package/templates/qoder/scripts/{common → domain/kg/graph}/graph_traverse.py +0 -0
  251. /package/templates/qoder/scripts/{common → domain/kg/server}/repowiki.py +0 -0
  252. /package/templates/qoder/scripts/{common → foundation/io}/atomicio.py +0 -0
  253. /package/templates/qoder/scripts/{secure-ls.js → validation/test/secure-ls.js} +0 -0
@@ -1,708 +1,871 @@
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
+ _f = _o.path.abspath(__file__)
5
+ for _ in range(10):
6
+ _f = _o.path.dirname(_f)
7
+ _cp = _o.path.join(_f, 'foundation')
8
+ if _o.path.isfile(_o.path.join(_cp, 'bootstrap.py')):
9
+ break
10
+ if _cp not in _s.path: _s.path.insert(0, _cp)
11
+ from bootstrap import setup; setup()
12
+ from foundation.core.paths import get_repo_root
13
+
14
+ """
15
+ kg.py — QODER 知识图谱统一 CLI 入口(全系列通用)
16
+
17
+ 把原本只在 QoderWork MCP (kg_mcp_server.py) 里的知识图谱能力,
18
+ 统一成一套命令行子命令。Qoder IDE / Quest / CLI 直接跑;QoderWork 仍可
19
+ MCP,本脚本是新增的并列通道(功能等价)。
20
+
21
+ 设计原则:
22
+ 1. 不复制逻辑 — 调底层: search_index / context_pack / graph_traverse /
23
+ kg_duckdb / repowiki / fill_prototype / gen_design_doc
24
+ 2. 全容错 DuckDB/索引缺失优雅降级,给修复提示,绝不抛栈给用户
25
+ 3. 参数统一 — 15 个子命令风格一致,对标 MCP 工具名
26
+ 4. 纯文本输出 — UTF-8,无日志噪音,AI 直接读
27
+
28
+ 用法:
29
+ python .qoder/scripts/kg.py <子命令> [参数]
30
+
31
+ python .qoder/scripts/kg.py search 考勤 --platform web
32
+ python .qoder/scripts/kg.py impact /asset
33
+ python .qoder/scripts/kg.py coverage
34
+ python .qoder/scripts/kg.py feature 资产管理
35
+ python .qoder/scripts/kg.py workflow assets
36
+ python .qoder/scripts/kg.py hop 资产管理 --depth 3
37
+ """
38
+ import io
39
+ import os
40
+ import re
41
+ import sys
42
+
43
+ # UTF-8 输出(Windows 终端默认 GBK 会乱码)
44
+ try:
45
+ sys.stdout.reconfigure(encoding='utf-8', errors='replace')
46
+ except Exception:
47
+ pass
48
+
49
+ SCRIPTS_DIR = get_repo_root()
50
+ BASE_DIR = get_repo_root()
51
+ sys.path.insert(0, SCRIPTS_DIR)
52
+ _KG_DIR = os.path.dirname(os.path.abspath(__file__))
53
+ if _KG_DIR not in sys.path: sys.path.insert(0, _KG_DIR) # 同包import
54
+
55
+
56
+ # ── 路径与索引新鲜度(与底层一致)──────────────────────────────────────
57
+ def _index_dir():
58
+ """获取 data/index 目录(延迟导入,避免顶层崩)。"""
59
+ try:
60
+ from foundation.core.paths import DATA_INDEX_DIR
61
+ return str(DATA_INDEX_DIR)
62
+ except Exception:
63
+ # 兜底: 常规布局
64
+ return os.path.join(BASE_DIR, 'data', 'index')
65
+
66
+
67
+ def _kg_db_path():
68
+ return os.path.join(_index_dir(), 'kg.duckdb')
69
+
70
+
71
+ def _has_duckdb():
72
+ """知识图谱 DuckDB 是否已构建。"""
73
+ return os.path.isfile(_kg_db_path())
74
+
75
+
76
+ def _hint_rebuild(action_cn='知识图谱'):
77
+ """DuckDB/索引缺失时的统一修复提示。"""
78
+ return (
79
+ "⚠️ %s未构建或缺失。\n"
80
+ "修复方法(任选其一):\n"
81
+ " 1. AI 说「初始化」(/wl-init) — 自动刷新索引\n"
82
+ " 2. 手动构建: python .qoder/scripts/kg_build.py\n"
83
+ " 3. 自检修复: python .qoder/scripts/init_doctor.py --fix\n"
84
+ "本次查询无法继续,但其他子命令(不依赖图谱的)仍可用。"
85
+ % action_cn
86
+ )
87
+
88
+
89
+ # ── 子命令实现 ────────────────────────────────────────────────────────
90
+
91
+ def cmd_search(args):
92
+ """搜代码: 按关键词找代码文件。"""
93
+ if not args:
94
+ return "用法: kg.py search <关键词> [--platform web|app]"
95
+ kw, plat = _split_kw_platform(args)
96
+ if not kw:
97
+ return "请提供搜索关键词"
98
+ from domain.kg.search import search_index as S
99
+ # search_keywords(query, platform=None) 自带中文扩展,不需外层 expand
100
+ return _capture(S.search_keywords, kw, plat) or "无匹配结果"
101
+
102
+
103
+ def cmd_api(args):
104
+ """搜 API 端点。"""
105
+ if not args:
106
+ return "用法: kg.py api <关键词>"
107
+ from domain.kg.search import search_index as S
108
+ return _capture(S.search_api, args[0]) or "无匹配 API"
109
+
110
+
111
+ def cmd_prd(args):
112
+ """搜 PRD(防重复造轮子)。"""
113
+ if not args:
114
+ return "用法: kg.py prd <关键词>"
115
+ from domain.kg.search import search_index as S
116
+ return _capture(S.search_prds, args[0]) or "无匹配 PRD"
117
+
118
+
119
+ def cmd_impact(args):
120
+ """影响分析: 改某接口影响哪些前端页面/按钮。"""
121
+ if not args:
122
+ return "用法: kg.py impact <端点或关键词> 例: /asset 或 export"
123
+ from domain.kg.search import search_index as S
124
+ return _capture(S.search_impact, args[0]) or "未找到匹配端点"
125
+
126
+
127
+ def cmd_context360(args):
128
+ """符号 360 度视图: 端点/函数/处理器的全部关联。"""
129
+ if not args:
130
+ return "用法: kg.py context360 <符号> 例: handleExport 或 /system/role"
131
+ from domain.kg.search import search_index as S
132
+ return _capture(S.search_context360, args[0]) or "未找到匹配符号"
133
+
134
+
135
+ def cmd_context(args):
136
+ """上下文打包: 一次返回关键词相关的全部上下文。支持 --role 角色裁剪。"""
137
+ if not args:
138
+ return ("用法: kg.py context <关键词> [--platform web|app] "
139
+ "[--role pm|design|dev|test|admin]")
140
+ rest = list(args)
141
+ role = ''
142
+ plat = ''
143
+ # 解析 --role / --platform
144
+ rest2 = []
145
+ i = 0
146
+ while i < len(rest):
147
+ a = rest[i]
148
+ if a == '--role' and i + 1 < len(rest):
149
+ role = rest[i + 1]; i += 2; continue
150
+ if a == '--platform' and i + 1 < len(rest):
151
+ plat = rest[i + 1]; i += 2; continue
152
+ rest2.append(a); i += 1
153
+ kw = rest2[0] if rest2 else ''
154
+ if not kw:
155
+ return "请提供业务关键词"
156
+ from domain.kg.search import context_pack as C
157
+ full = _capture(C._compute_and_print, kw, plat, '')
158
+ if not full:
159
+ return "无匹配上下文"
160
+ if role:
161
+ return _filter_by_role(full, role)
162
+ return full
163
+
164
+
165
+ def cmd_prefetch(args):
166
+ """需求上下文预测 + 批量预取: 输入一句需求描述, 一次返回多词合并的上下文。
167
+
168
+ 治"串行试探式搜索"(查一个词不命中 → 换词再查 6+ 次)的轮次爆炸。
169
+ 支持 --platform。
170
+ """
171
+ if not args:
172
+ return ("用法: kg.py prefetch \"<需求描述>\" [--platform web|app]\n"
173
+ "例: kg.py prefetch \"保险单OCR识别标注对比图\" --platform web")
174
+ rest = list(args)
175
+ plat = ''
176
+ rest2 = []
177
+ i = 0
178
+ while i < len(rest):
179
+ a = rest[i]
180
+ if a == '--platform' and i + 1 < len(rest):
181
+ plat = rest[i + 1]; i += 2; continue
182
+ rest2.append(a); i += 1
183
+ query = ' '.join(rest2) if rest2 else ''
184
+ if not query:
185
+ return "请提供需求描述(一句话)"
186
+ from domain.kg.search import prefetch as P
187
+ primary_cn, en_words = P.predict_words(query)
188
+ if not en_words:
189
+ # 降级: 预测不出词单词 context_pack
190
+ from domain.kg.search import context_pack as C
191
+ return _capture(C._compute_and_print, query, plat, '') or "无匹配上下文"
192
+ targets = []
193
+ if plat and plat.lower() not in ('both', '两端'):
194
+ targets = [P.PLATFORM_MAP.get(plat.lower(), plat)]
195
+ elif plat:
196
+ targets = sorted(set(P.PLATFORM_MAP.values()))
197
+ collected = P._collect(targets, en_words)
198
+ return _capture(P._print_merged, query, plat, primary_cn, en_words, collected) or "无匹配上下文"
199
+
200
+
201
+ def cmd_coverage(args):
202
+ """功能×测试覆盖矩阵: 哪些功能有测试,哪些是盲区。"""
203
+ from domain.kg.graph.graph_traverse import CodeGraph
204
+ g = CodeGraph(_index_dir())
205
+ return g.coverage_matrix() or "无法生成覆盖矩阵(索引可能为空)"
206
+
207
+
208
+ def cmd_feature(args):
209
+ """功能画像: 一个功能的完整画像(端点+按钮+测试)。
210
+
211
+ DuckDB 优先(DuckDB 数据最全);DuckDB 缺失时回退到 CodeGraph 的 JSON 路径。
212
+ """
213
+ if not args:
214
+ return "用法: kg.py feature <功能名> 例: 资产管理 asset system"
215
+ feat = args[0]
216
+ # DuckDB 优先(数据最全,coverage 已验证可用)
217
+ if _has_duckdb():
218
+ try:
219
+ import duckdb
220
+ except ImportError:
221
+ return "未安装 duckdb。请运行: pip install duckdb"
222
+ con = duckdb.connect(_kg_db_path(), read_only=True)
223
+ try:
224
+ result = _feature_duckdb(con, feat)
225
+ if result:
226
+ return result
227
+ finally:
228
+ try: con.close()
229
+ except Exception: pass
230
+ # 回退: CodeGraph JSON 实现(entity-registry.json 通常不存在)
231
+ from domain.kg.graph.graph_traverse import CodeGraph
232
+ g = CodeGraph(_index_dir())
233
+ return g.feature_overview(feat) or "未找到功能 '%s'" % feat
234
+
235
+
236
+ def _feature_duckdb(con, query):
237
+ """从 DuckDB 查功能画像(feature endpoints/buttons/tests)。
238
+
239
+ 别名解析: aliases 精确 → canonical/cn 模糊 → entity_id 含关键词。
240
+ """
241
+ q = (query or '').strip()
242
+ # 1) 解析为 FEATURE entity_id
243
+ target = None
244
+ resolved = con.execute(
245
+ "SELECT entity_id FROM aliases WHERE alias = ? "
246
+ "AND entity_id IN (SELECT id FROM entities WHERE type='FEATURE')",
247
+ [q.lower()]
248
+ ).fetchone()
249
+ if resolved:
250
+ target = resolved[0]
251
+ else:
252
+ # canonical / cn 模糊
253
+ row = con.execute(
254
+ "SELECT id FROM entities WHERE type='FEATURE' "
255
+ "AND (LOWER(canonical)=? OR cn=? OR LOWER(canonical) LIKE '%'||?||'%' OR cn LIKE '%'||?||'%') "
256
+ "LIMIT 1",
257
+ [q.lower(), q, q.lower(), q]
258
+ ).fetchone()
259
+ if row:
260
+ target = row[0]
261
+ if not target:
262
+ # 列出可用 feature 帮用户选
263
+ feats = con.execute(
264
+ "SELECT canonical, cn FROM entities WHERE type='FEATURE' AND cn<>'' ORDER BY cn LIMIT 30"
265
+ ).fetchall()
266
+ if not feats:
267
+ # entities 表为空 — 语义层未构建 (需要 build_entity_registry 等脚本, 非数据缺失)
268
+ total = con.execute("SELECT COUNT(*) FROM entities").fetchone()[0]
269
+ if total == 0:
270
+ return ("未找到功能 '%s'。知识图谱语义层 (entities) 为空 "
271
+ "需要管理员构建: npx @hupan56/wlkj run build_style_index.py "
272
+ "(生成 entity-registry 后由 kg_duckdb 导入)。" % q)
273
+ cands = ', '.join('%s(%s)' % (c, cn) if cn and cn != c else c for c, cn in feats)
274
+ return "未找到功能 '%s'。可用功能: %s" % (q, cands)
275
+
276
+ # 2) feature 基本信息
277
+ info = con.execute(
278
+ "SELECT canonical, cn FROM entities WHERE id=?", [target]
279
+ ).fetchone()
280
+ canon, cn = info if info else (target, target)
281
+
282
+ # 3) 统计端点 / 按钮
283
+ endpoints = con.execute(
284
+ "SELECT to_id FROM edges WHERE from_id=? AND edge_type='has_endpoint'", [target]
285
+ ).fetchall()
286
+ buttons = con.execute(
287
+ "SELECT to_id FROM edges WHERE from_id=? AND edge_type='has_button'", [target]
288
+ ).fetchall()
289
+
290
+ # 4) 关联测试: feature 的端点 -(handled_by)-> controller -(tested_by)-> test
291
+ try:
292
+ tests = con.execute("""
293
+ SELECT DISTINCT tb.to_id FROM edges he
294
+ JOIN edges tb ON tb.from_id = he.to_id AND tb.edge_type='tested_by'
295
+ WHERE he.from_id=? AND he.edge_type='handled_by'
296
+ """, [target]).fetchall()
297
+ except Exception:
298
+ tests = []
299
+
300
+ ep_ids = [e[0] for e in endpoints]
301
+ btn_ids = [b[0] for b in buttons]
302
+ test_ids = [t[0] for t in tests]
303
+
304
+ # 5) 组装画像
305
+ out = ['## 功能画像: %s (%s)' % (cn, canon)]
306
+ out.append(' 端点 %d 个 | 按钮操作 %d 个 | 关联测试 %d 个' % (
307
+ len(ep_ids), len(btn_ids), len(test_ids)))
308
+ out.append('')
309
+
310
+ out.append('### 端点 (%d)' % len(ep_ids))
311
+ for eid in ep_ids[:10]:
312
+ out.append(' %s' % eid)
313
+ if len(ep_ids) > 10:
314
+ out.append(' ... 还有 %d 个' % (len(ep_ids) - 10))
315
+ out.append('')
316
+
317
+ out.append('### 按钮操作 (%d)' % len(btn_ids))
318
+ # 提取按钮的操作动词(handleAdd/handleDel 等)
319
+ verbs = {}
320
+ for bid in btn_ids:
321
+ # bid 形如 B:path/to/file.vue:handleAdd
322
+ if ':' in bid:
323
+ handler = bid.rsplit(':', 1)[-1]
324
+ else:
325
+ handler = bid
326
+ verbs[handler] = verbs.get(handler, 0) + 1
327
+ top_verbs = sorted(verbs.items(), key=lambda x: -x[1])[:15]
328
+ out.append(' 操作分布: %s' % ', '.join('%s×%d' % (v, n) for v, n in top_verbs))
329
+ out.append('')
330
+
331
+ out.append('### 关联测试 (%d)' % len(test_ids))
332
+ shown = 0
333
+ for tid in test_ids:
334
+ if shown >= 6:
335
+ out.append(' ... 还有 %d 个' % (len(test_ids) - 6))
336
+ break
337
+ # tid 形如 T:ClassName.method,查 tests 表拿断言
338
+ if tid.startswith('T:'):
339
+ ref = tid[2:]
340
+ cls = method = ''
341
+ if '.' in ref:
342
+ cls, method = ref.rsplit('.', 1)
343
+ trow = con.execute(
344
+ "SELECT display_name, assertions FROM tests WHERE class=? AND method=? LIMIT 1",
345
+ [cls, method]
346
+ ).fetchone()
347
+ if trow:
348
+ dn, asserts = trow
349
+ import json as _json
350
+ try:
351
+ a_list = _json.loads(asserts) if isinstance(asserts, str) else asserts
352
+ a_str = ', '.join(a_list[:3]) if a_list else ''
353
+ except Exception:
354
+ a_str = ''
355
+ out.append(' %s — %s%s' % (ref, dn or '', (' [断言: %s]' % a_str) if a_str else ''))
356
+ else:
357
+ out.append(' %s' % ref)
358
+ else:
359
+ out.append(' %s' % tid)
360
+ shown += 1
361
+
362
+ if len(test_ids) == 0:
363
+ out.append(' ⚠️ 该功能无关联测试,是回归测试盲区')
364
+
365
+ return '\n'.join(out)
366
+
367
+
368
+ def cmd_workflow(args):
369
+ """业务流程链: 一个模块的完整操作链(query→create→audit→...)。
370
+
371
+ 优先匹配英文模块名(assets/cost/hr/...),中文输入返回候选。
372
+ 数据源: DuckDB workflows 表。
373
+ """
374
+ if not args:
375
+ return ("用法: kg.py workflow <模块名> 例: assets / cost / hr\n"
376
+ "可选模块: assets/cost/hr/maintenance/operation/"
377
+ "qualityAndSafety/veh/settings/dashboard/flow/systems/monitor/quasafe")
378
+ module = args[0]
379
+ if not _has_duckdb():
380
+ return _hint_rebuild('业务流程数据(DuckDB)')
381
+ try:
382
+ import duckdb
383
+ except ImportError:
384
+ return ("未安装 duckdb。请运行: pip install duckdb")
385
+ con = duckdb.connect(_kg_db_path(), read_only=True)
386
+ try:
387
+ return _workflow_query(con, module)
388
+ finally:
389
+ try: con.close()
390
+ except Exception: pass
391
+
392
+
393
+ def _workflow_query(con, q):
394
+ """workflows 表查询 — v3.0 f3: 转发到 common.kg_capabilities (单一真源, CLI/MCP共用)。"""
395
+ from domain.kg.kg_capabilities import workflow_query
396
+ return workflow_query(con, q)
397
+
398
+
399
+ def cmd_hop(args):
400
+ """多跳遍历: 从一个符号出发,递归 N 跳找全部可达实体。
401
+
402
+ 知识图谱核心能力。支持别名解析(传中文/端点/函数名自动转 entity_id)。
403
+ """
404
+ if not args:
405
+ return "用法: kg.py hop <符号> [--depth 1-5] 例: 资产管理 /asset/list"
406
+ symbol, depth = _split_symbol_depth(args)
407
+ if not _has_duckdb():
408
+ return _hint_rebuild('知识图谱(DuckDB)')
409
+ try:
410
+ import duckdb
411
+ except ImportError:
412
+ return "未安装 duckdb。请运行: pip install duckdb"
413
+ from domain.kg.storage import kg_duckdb as K
414
+ con = duckdb.connect(_kg_db_path(), read_only=True)
415
+ try:
416
+ # v3.0 f3: 核心查询转发到 common.kg_capabilities.hop_query (CLI/MCP共用)
417
+ from domain.kg.kg_capabilities import hop_query
418
+ return hop_query(con, symbol, depth, multi_hop_fn=K.multi_hop)
419
+ finally:
420
+ try: con.close()
421
+ except Exception: pass
422
+
423
+
424
+ def cmd_wiki(args):
425
+ """Repo Wiki 搜索 v3.0 f3: 转发到 common.kg_capabilities.wiki_query (CLI/MCP共用)。"""
426
+ if not args:
427
+ return "用法: kg.py wiki <关键词>"
428
+ from domain.kg.kg_capabilities import wiki_query
429
+ result = wiki_query(args[0])
430
+ # CLI 额外提示
431
+ if 'matching' in result:
432
+ result += '\n\n 提示: 读 wiki 文档(带行号引用)比读源码更省 token'
433
+ return result
434
+
435
+
436
+ def cmd_fill_prototype(args):
437
+ """原型预填: 用真实代码数据自动填模板,输出 80% 完成的 HTML 草稿。"""
438
+ if not args:
439
+ return "用法: kg.py fill-prototype <关键词> [--platform web|app]"
440
+ kw, plat = _split_kw_platform(args)
441
+ plat = plat or 'web'
442
+ import subprocess as _sp
443
+ from foundation.core.paths import find_script
444
+ script = find_script('fill_prototype.py')
445
+ if script is None:
446
+ return "fill_prototype.py 未找到 (脚本迁移后路径未更新)"
447
+ try:
448
+ r = _sp.run([sys.executable, str(script), kw, '--platform', plat],
449
+ capture_output=True, text=True, encoding='utf-8',
450
+ errors='replace', timeout=30, cwd=str(script.parent))
451
+ return r.stdout or "fill_prototype 无输出(可能索引缺失)"
452
+ except Exception as e:
453
+ return "[error] fill_prototype: %s" % str(e)
454
+
455
+
456
+ def cmd_design_system(args):
457
+ """设计系统查询: 返回项目完整设计规范(布局指纹/颜色token/组件表/图标)。"""
458
+ plat = ''
459
+ if args and args[0] in ('--platform', '-p') and len(args) > 1:
460
+ plat = args[1]
461
+ import subprocess as _sp
462
+ design_md = os.path.join(BASE_DIR, 'data', 'design', 'DESIGN.md')
463
+ if not os.path.isfile(design_md):
464
+ from foundation.core.paths import find_script
465
+ gen_script = find_script('gen_design_doc.py')
466
+ if gen_script is not None:
467
+ _sp.run([sys.executable, str(gen_script)], capture_output=True, timeout=30)
468
+ if os.path.isfile(design_md):
469
+ content = open(design_md, encoding='utf-8').read()
470
+ if plat:
471
+ lines = content.split('\n')
472
+ result = []
473
+ in_section = False
474
+ for line in lines:
475
+ if line.startswith('### ') and plat.lower() in line.lower():
476
+ in_section = True
477
+ elif line.startswith('### ') and in_section:
478
+ break
479
+ if in_section:
480
+ result.append(line)
481
+ return '\n'.join(result) if result else content
482
+ return content
483
+ return "DESIGN.md 不存在且生成失败。请手动跑: python .qoder/scripts/design/gen_design_doc.py"
484
+
485
+
486
+ def cmd_semantic(args):
487
+ """语义搜索: 用自然语言找代码/API (概念检索, 不依赖精确关键词)。
488
+
489
+ 需先构建 embedding 向量 (管理员): kg_semantic.py build-embeddings。
490
+ 模型未装时给出安装提示, 不崩溃。
491
+ """
492
+ if not args:
493
+ return ("用法: kg.py semantic <自然语言查询> [--top N]\n"
494
+ " 语义检索: '报销'能命中 expense/reimburse\n"
495
+ " 需先构建: python .qoder/scripts/domain/kg/graph/kg_semantic.py build-embeddings")
496
+ query = args[0]
497
+ top_k = 10
498
+ if '--top' in args:
499
+ idx = args.index('--top')
500
+ if idx + 1 < len(args):
501
+ try:
502
+ top_k = int(args[idx + 1])
503
+ except ValueError:
504
+ pass
505
+ from domain.kg.graph.kg_semantic import semantic_search, embedding_status
506
+ results = semantic_search(query, top_k=top_k)
507
+ if results is None:
508
+ st = embedding_status()
509
+ if not st['available']:
510
+ return ("语义检索未启用: sentence-transformers 未安装。\n"
511
+ "启用: pip install sentence-transformers (95MB模型首次自动下载)")
512
+ elif st['embeddings_count'] == 0:
513
+ return ("语义检索未构建向量。请管理员运行:\n"
514
+ " python .qoder/scripts/domain/kg/graph/kg_semantic.py build-embeddings")
515
+ if not results:
516
+ return "无匹配结果"
517
+ out = ["语义搜索: %s (top %d)" % (query, len(results))]
518
+ for eid, text, score in results:
519
+ out.append(" %.3f %s" % (score, text[:70]))
520
+ return '\n'.join(out)
521
+
522
+
523
+ # ── 中文文案/报错溯源 (grep-text): 单进程安全兜底, 替代 findstr/s 全盘扫描 ──
524
+ # 设计动机: 用户查"车辆不存在""项目车号"这类中文文案时, kg.py 的代码标识符索引
525
+ # 天然查不到 (索引按英文标识符建)。AI 若兜底去 findstr /s 或 os.walk 全库
526
+ # (8973+ 文件) → Windows 卡死 + 疯狂弹 cmd 窗口。本命令是这类需求的唯一安全通道:
527
+ # 单进程、限定 data/code、限定扩展名、有行数上限, 绝不递归弹子进程。
528
+ _GREP_EXTS = ('.java', '.vue', '.ts', '.tsx', '.js', '.jsx', '.xml', '.html',
529
+ '.json', '.sql', '.properties', '.yml', '.yaml', '.ftl', '.txt')
530
+
531
+
532
+ def cmd_grep_text(args):
533
+ """中文文案/报错溯源: data/code 里搜中文短语 (报错文案/字段标签/业务词)
534
+
535
+ 单进程 Python, 限定扩展名, 上限 200 命中 + 30 文件。绝不弹子进程窗口。
536
+ 用法: kg.py grep-text <中文短语> [--ext java,vue] [--limit N] [--platform web|app]
537
+ """
538
+ if not args:
539
+ return ("用法: kg.py grep-text <中文短语> [选项]\n"
540
+ " 在 data/code 里搜中文文案/报错/字段标签 (kg.py search 查不到中文时用这个)\n"
541
+ " --ext java,vue 限定文件类型 (默认: java/vue/ts/xml/json...)\n"
542
+ " --limit N 最多返回 N 条命中 (默认 50)\n"
543
+ " --platform web|app 只搜某平台源码\n"
544
+ "示例: kg.py grep-text 车辆不存在")
545
+ import os as _os
546
+ # kg.py 在 .qoder/scripts/domain/kg/ 下, 往上 4 级到 repo 根
547
+ here = _os.path.dirname(_os.path.abspath(__file__)) # .../domain/kg/
548
+ base = _os.path.dirname(_os.path.dirname(_os.path.dirname(_os.path.dirname(here)))) # repo 根
549
+ code_root = _os.path.join(base, 'data', 'code')
550
+ if not _os.path.isdir(code_root):
551
+ return ('data/code 不存在 (源码未克隆)。跑 python .qoder/scripts/deployment/setup/init_doctor.py --fix')
552
+
553
+ query = args[0]
554
+ # 解析选项
555
+ exts = set(_GREP_EXTS)
556
+ limit = 50
557
+ user_limit = None # 记录用户是否显式传了 --limit (决定是否启用 early_stop)
558
+ plat = ''
559
+ i = 1
560
+ while i < len(args):
561
+ a = args[i]
562
+ if a == '--ext' and i + 1 < len(args):
563
+ exts = set('.' + e.lstrip('.') for e in args[i + 1].split(',') if e.strip())
564
+ i += 2; continue
565
+ if a == '--limit' and i + 1 < len(args):
566
+ try:
567
+ limit = max(1, int(args[i + 1]))
568
+ user_limit = limit # 用户显式指定了, early_stop 不再介入
569
+ except ValueError: pass
570
+ i += 2; continue
571
+ if a == '--platform' and i + 1 < len(args):
572
+ plat = args[i + 1]; i += 2; continue
573
+ i += 1
574
+
575
+ # 平台 → 子目录过滤
576
+ proj_dirs = []
577
+ if not plat:
578
+ for d in sorted(_os.listdir(code_root)):
579
+ if _os.path.isdir(_os.path.join(code_root, d)) and not d.startswith('.'):
580
+ proj_dirs.append(d)
581
+ elif plat in ('web', 'pc'):
582
+ proj_dirs = [d for d in ('fywl-ui',) if _os.path.isdir(_os.path.join(code_root, d))]
583
+ elif plat in ('app', 'h5'):
584
+ proj_dirs = [d for d in ('Carmg-H5',) if _os.path.isdir(_os.path.join(code_root, d))]
585
+ else:
586
+ proj_dirs = [d for d in sorted(_os.listdir(code_root))
587
+ if _os.path.isdir(_os.path.join(code_root, d)) and not d.startswith('.')]
588
+
589
+ hits = []
590
+ files_hit = 0
591
+ scanned = 0
592
+ import time as _time
593
+ deadline = _time.monotonic() + 15 # 硬超时 15s: data/code 大目录上避免 30s+ 卡死
594
+ timed_out = False
595
+ # 够用即停阈值: 仅当用户没显式指定 --limit 时启用 (默认场景快速溯源)。
596
+ # 用户传了 --limit 就完全尊重它, 不被 early_stop 截断 (原 max(10,min(limit,30))
597
+ # 会把 --limit 50 截成 30、把 --limit 5 抬到 10, 违背用户契约)。
598
+ early_stop = 30 if user_limit is None else None
599
+ for proj in proj_dirs:
600
+ proj_path = _os.path.join(code_root, proj)
601
+ for dirpath, dirnames, filenames in _os.walk(proj_path):
602
+ # 跳过明显的依赖/构建目录, 加速
603
+ dirnames[:] = [d for d in dirnames if d not in
604
+ ('node_modules', '.git', 'target', 'dist', 'build', '.idea', '__pycache__')]
605
+ for fn in filenames:
606
+ # 文件级超时检查: 避免单个大文件卡死目录内循环 (15s 硬超时在目录级
607
+ # 检查时形同虚设)。已找到结果才超时退出, 没找到继续扫。
608
+ if hits and _time.monotonic() > deadline:
609
+ timed_out = True
610
+ break
611
+ ext = _os.path.splitext(fn)[1].lower()
612
+ if ext not in exts:
613
+ continue
614
+ fp = _os.path.join(dirpath, fn)
615
+ # 跳过大文件 (>500KB 多半是生成/压缩文件, 拖慢扫描)
616
+ try:
617
+ if _os.path.getsize(fp) > 500_000:
618
+ continue
619
+ except OSError:
620
+ continue
621
+ scanned += 1
622
+ done = False
623
+ try:
624
+ with open(fp, 'r', encoding='utf-8', errors='ignore') as fh:
625
+ for lineno, line in enumerate(fh, 1):
626
+ # 逐文件行级超时检查: 极大文件逐行读也能及时退出
627
+ if lineno % 20000 == 0 and hits and _time.monotonic() > deadline:
628
+ timed_out = True
629
+ done = True
630
+ break
631
+ if query in line:
632
+ rel = _os.path.relpath(fp, code_root).replace('\\', '/')
633
+ snippet = line.strip()[:120]
634
+ hits.append((rel, lineno, snippet))
635
+ if len(hits) >= limit:
636
+ done = True
637
+ break
638
+ # 够用即停 (仅默认场景): 与 limit 同级行内检查,
639
+ # 避免单文件多行命中跨过 early_stop (原34>30问题)
640
+ if early_stop is not None and len(hits) >= early_stop:
641
+ done = True
642
+ break
643
+ except (OSError, UnicodeDecodeError):
644
+ pass
645
+ if done or len(hits) >= limit:
646
+ break
647
+ # 够用即停 (仅默认场景; 用户显式 limit 时 early_stop=None 跳过)
648
+ if early_stop is not None and len(hits) >= early_stop:
649
+ break
650
+ if timed_out or len(hits) >= limit or (early_stop is not None and len(hits) >= early_stop):
651
+ break
652
+ if timed_out or len(hits) >= limit or (early_stop is not None and len(hits) >= early_stop):
653
+ break
654
+
655
+ # 统计命中文件数 (去重)
656
+ files_hit = len(set(h[0] for h in hits))
657
+ if not hits:
658
+ return ('未找到 "%s" (扫描 %d 个文件, %s)\n'
659
+ '提示: 换近义词, 或用 kg.py semantic "%s" 语义检索' % (
660
+ query, scanned, '/'.join(proj_dirs) or 'data/code', query))
661
+ out = ['grep-text: "%s" %d 命中 / %d 文件 (扫描 %d 文件%s)' % (
662
+ query, len(hits), files_hit, scanned, ', ⏱15s超时已返回部分结果' if timed_out else '')]
663
+ last_file = ''
664
+ for rel, lineno, snippet in hits:
665
+ if rel != last_file:
666
+ out.append('\n■ %s' % rel)
667
+ last_file = rel
668
+ out.append(' L%d: %s' % (lineno, snippet))
669
+ return '\n'.join(out)
670
+
671
+
672
+ # ── 工具函数 ──────────────────────────────────────────────────────────
673
+
674
+ def _capture(func, *args, **kwargs):
675
+ """运行函数,捕获其 stdout 输出(底层脚本用 print 输出)。"""
676
+ old = sys.stdout
677
+ buf = io.StringIO()
678
+ sys.stdout = buf
679
+ try:
680
+ func(*args, **kwargs)
681
+ except Exception as e:
682
+ buf.write('\n[error] %s' % str(e))
683
+ finally:
684
+ sys.stdout = old
685
+ return buf.getvalue().strip()
686
+
687
+
688
+ def _split_kw_platform(args):
689
+ """从参数里拆出 keyword 和 --platform 值。"""
690
+ rest = list(args)
691
+ plat = ''
692
+ out = []
693
+ i = 0
694
+ while i < len(rest):
695
+ a = rest[i]
696
+ if a == '--platform' and i + 1 < len(rest):
697
+ plat = rest[i + 1]; i += 2; continue
698
+ out.append(a); i += 1
699
+ kw = out[0] if out else ''
700
+ return kw, plat
701
+
702
+
703
+ def _split_symbol_depth(args):
704
+ """从参数里拆出 symbol 和 --depth 值(默认 3)。"""
705
+ rest = list(args)
706
+ depth = 3
707
+ out = []
708
+ i = 0
709
+ while i < len(rest):
710
+ a = rest[i]
711
+ if a == '--depth' and i + 1 < len(rest):
712
+ try: depth = int(rest[i + 1])
713
+ except (ValueError, TypeError): depth = 3
714
+ i += 2; continue
715
+ out.append(a); i += 1
716
+ return (out[0] if out else ''), max(1, min(5, depth))
717
+
718
+
719
+ # Role-based section filtering for context_pack output
720
+ # v3.0: 单一真源移到 common/roles.py, 消除与 kg_mcp_server.py 的逐字重复
721
+ try:
722
+ from foundation.identity.roles import ROLE_SECTIONS, filter_by_role as _filter_by_role_imported
723
+ _filter_by_role = _filter_by_role_imported # 保持旧调用名 _filter_by_role 兼容
724
+ except Exception:
725
+ # 回退: 内联定义 (common 不可用时, 与 common/roles.py 保持一致)
726
+ ROLE_SECTIONS = {
727
+ 'pm': {'1', '2', '2b', '3', '4', '5', '7'},
728
+ 'design': {'2', '2b', '3', '6'},
729
+ 'dev': {'1', '3', '5', '7'},
730
+ 'test': {'1', '4', '5'},
731
+ 'admin': {'4', '5'},
732
+ }
733
+
734
+ def _filter_by_role(text, role):
735
+ visible = ROLE_SECTIONS.get((role or '').lower())
736
+ if not visible:
737
+ return text
738
+ sections = re.split(r'(\n## \d+[ab]?\.)', text)
739
+ result = sections[0]
740
+ i = 1
741
+ while i < len(sections):
742
+ header = sections[i]
743
+ num_m = re.match(r'\n## (\d+[ab]?)\.', header)
744
+ if num_m and num_m.group(1) in visible:
745
+ result += header
746
+ if i + 1 < len(sections):
747
+ result += sections[i + 1]
748
+ i += 2
749
+ else:
750
+ i += 2
751
+ return result.strip()
752
+
753
+
754
+ # ── 子命令注册表 ──────────────────────────────────────────────────────
755
+ # (子命令名, 处理函数, 中文说明)
756
+ COMMANDS = [
757
+ ('search', cmd_search, '搜代码: 按关键词找代码文件'),
758
+ ('api', cmd_api, '搜 API 端点'),
759
+ ('prd', cmd_prd, '搜 PRD(防重复造轮子)'),
760
+ ('impact', cmd_impact, '影响分析: 改某接口影响哪些页面/按钮'),
761
+ ('context360', cmd_context360, '符号 360 度视图(端点/函数/处理器)'),
762
+ ('context', cmd_context, '上下文打包(代码+页面+字段+PRD+API,可 --role 角色裁剪)'),
763
+ ('prefetch', cmd_prefetch, '需求上下文预测+批量预取(一句需求→多词合并,治串行搜索)'),
764
+ ('coverage', cmd_coverage, '功能×测试覆盖矩阵(标出回归盲区)'),
765
+ ('feature', cmd_feature, '功能画像(端点+按钮+测试+页面)'),
766
+ ('workflow', cmd_workflow, '业务流程链(query→create→audit→...)'),
767
+ ('hop', cmd_hop, '多跳遍历(知识图谱核心,从一符号递归 N 跳)'),
768
+ ('wiki', cmd_wiki, 'Repo Wiki 搜索(模块语义文档)'),
769
+ ('fill-prototype', cmd_fill_prototype, '原型预填(真实数据填模板,输出 80% HTML)'),
770
+ ('design-system', cmd_design_system, '设计系统查询(布局指纹/颜色token/组件表)'),
771
+ ('semantic', cmd_semantic, '语义搜索(概念检索:查"报销"命中 expense)'),
772
+ ('grep-text', cmd_grep_text, '中文文案/报错溯源(findstr 替代,单进程不弹窗)'),
773
+ ]
774
+
775
+
776
+ def _usage():
777
+ """打印用法清单。"""
778
+ lines = [
779
+ 'kg.py — QODER 知识图谱统一 CLI(全系列通用)',
780
+ '',
781
+ '用法: python .qoder/scripts/kg.py <子命令> [参数]',
782
+ '',
783
+ '16 个子命令:',
784
+ ]
785
+ for name, _, desc in COMMANDS:
786
+ lines.append(' %-16s %s' % (name, desc))
787
+ lines += [
788
+ '',
789
+ '示例:',
790
+ ' kg.py search 考勤 --platform web',
791
+ ' kg.py prefetch "保险单OCR识别" --platform web # 写PRD前先批量预取',
792
+ ' kg.py impact /asset # 改 /asset 影响谁',
793
+ ' kg.py coverage # 哪些功能没测试',
794
+ ' kg.py feature 资产管理 # 资产管理功能画像',
795
+ ' kg.py workflow assets # 资产模块业务流程',
796
+ ' kg.py hop 资产管理 --depth 3 # 从资产管理出发 3 跳遍历',
797
+ ' kg.py context 车辆 --role pm # PM 视角的上下文打包',
798
+ ' kg.py grep-text 车辆不存在 # 中文报错文案溯源 (findstr 安全替代)',
799
+ '',
800
+ 'QoderWork 环境: AI 也会直接调 MCP 工具(mcp__qoder-knowledge-graph__*),功能等价。',
801
+ ]
802
+ return '\n'.join(lines)
803
+
804
+
805
+ def _try_daemon(cmd, rest):
806
+ """尝试走 daemon 热路径。命中返回 True (已输出结果), 未命中返回 False (回退冷路径)。
807
+
808
+ daemon 常驻模型+连接, semantic 26.8s→0.08s。连不上/出错则透明回退。
809
+ """
810
+ try:
811
+ from kgd import kgd_client, DAEMON_COMMANDS
812
+ except Exception:
813
+ return False # kgd 模块不可用 → 回退
814
+ if cmd not in DAEMON_COMMANDS:
815
+ return False # 该命令不走 daemon
816
+ res = kgd_client.call(cmd, rest)
817
+ if res is None:
818
+ return False # daemon 不在线/出错 → 回退
819
+ text, _secs = res
820
+ print(text)
821
+ return True
822
+
823
+
824
+ def main(argv=None):
825
+ argv = argv if argv is not None else sys.argv[1:]
826
+
827
+ # 解析 --no-daemon: 强制冷路径 (调试用)。剥离后不影响后续参数解析。
828
+ use_daemon = True
829
+ if '--no-daemon' in argv:
830
+ use_daemon = False
831
+ argv = [a for a in argv if a != '--no-daemon']
832
+
833
+ if not argv or argv[0] in ('-h', '--help', 'help'):
834
+ print(_usage())
835
+ return 0
836
+ cmd = argv[0]
837
+ rest = argv[1:]
838
+ # 查找子命令
839
+ handler = None
840
+ for name, fn, _ in COMMANDS:
841
+ if name == cmd:
842
+ handler = fn
843
+ break
844
+ if handler is None:
845
+ print("未知子命令: %s" % cmd)
846
+ print(_usage())
847
+ return 1
848
+
849
+ # ⚡ 优先走 daemon 热路径 (模型/连接已常驻, 秒回)
850
+ if use_daemon and _try_daemon(cmd, rest):
851
+ return 0
852
+
853
+ # 冷路径: 直接执行 handler (daemon 不在线 / --no-daemon / 不支持的命令)
854
+ # semantic 在 daemon 不在线时给加速提示 (它单次要 ~27s)
855
+ if cmd == 'semantic' and use_daemon:
856
+ print('[提示] daemon 未运行, semantic 将冷启动 (~27s)。')
857
+ print(' 加速: python .qoder/scripts/domain/kg/server/kgd.py start (常驻后 semantic → 0.08s)')
858
+ # 执行(全容错,绝不抛栈)
859
+ try:
860
+ result = handler(rest)
861
+ if result:
862
+ print(result)
863
+ except Exception as e:
864
+ print("[error] %s 执行失败: %s" % (cmd, str(e)))
865
+ print("如果是索引/图谱问题,尝试: python .qoder/scripts/init_doctor.py --fix")
866
+ return 1
867
+ return 0
868
+
869
+
870
+ if __name__ == '__main__':
871
+ sys.exit(main())