@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,5 +1,17 @@
1
1
  #!/usr/bin/env python3
2
2
  # -*- coding: utf-8 -*-
3
+ # v3.0 路径自举: 引导到 common/bootstrap, 统一 sys.path 逻辑
4
+ import os as _o, sys as _s
5
+ _f = _o.path.abspath(__file__)
6
+ for _ in range(10):
7
+ _f = _o.path.dirname(_f)
8
+ _cp = _o.path.join(_f, 'foundation')
9
+ if _o.path.isfile(_o.path.join(_cp, 'bootstrap.py')):
10
+ break
11
+ if _cp not in _s.path: _s.path.insert(0, _cp)
12
+ from bootstrap import setup; setup()
13
+ from foundation.core.paths import get_repo_root
14
+
3
15
  """
4
16
  Search knowledge index with platform filtering and PRD search.
5
17
 
@@ -27,10 +39,10 @@ except (AttributeError, TypeError, OSError, IOError):
27
39
  pass
28
40
 
29
41
  sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
30
- from common.terms import CN_MAP, BUSINESS_PATH_MAP, CN_TO_EN, get_platform_map, expand_chinese_query
42
+ from foundation.integrations.terms import CN_MAP, BUSINESS_PATH_MAP, CN_TO_EN, get_platform_map, expand_chinese_query
31
43
  PLATFORM_MAP = get_platform_map()
32
44
 
33
- BASE = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
45
+ BASE = get_repo_root()
34
46
  INDEX_DIR = os.path.join(BASE, 'data', 'index')
35
47
 
36
48
  # Substring matching against keys shorter than this produces too much noise
@@ -82,17 +94,45 @@ def _get_duckdb():
82
94
  return None
83
95
 
84
96
 
85
- def _load_keyword_with_cache(con):
86
- """加载 code-keyword (11836 行), pickle 缓存层。
97
+ # 第二个 DuckDB (kg_db.duckdb, 数据库图谱) 的惰性连接缓存。
98
+ # context_pack 8 段每次都新开连接读这个库, 与上面的 kg.duckdb 单例不对称,
99
+ # 每次调用多一次连接开销。这里镜像 _get_duckdb 的单例模式, 供 context_pack 复用。
100
+ _kgdb_con = None
101
+
102
+
103
+ def _get_kg_db():
104
+ global _kgdb_con
105
+ if _kgdb_con is not None:
106
+ return _kgdb_con
107
+ db_path = os.path.join(INDEX_DIR, 'kg_db.duckdb')
108
+ if not os.path.isfile(db_path):
109
+ return None
110
+ try:
111
+ import duckdb
112
+ _kgdb_con = duckdb.connect(db_path, read_only=True)
113
+ return _kgdb_con
114
+ except Exception:
115
+ return None
116
+
117
+
118
+ def _load_with_pickle_cache(con, cache_name, loader_fn, empty_sentinel=None):
119
+ """通用 pickle/mtime 缓存层 (抽自 _load_keyword_with_cache, 推广到全部索引)。
87
120
 
88
- 性能: DuckDB SELECT + Python 循环重建 = ~180ms;
89
- pickle.load = ~37ms (提速 5x, 实测数据一致)
121
+ 性能: ui-style 等索引每次从 DuckDB 全量重建 (3 次查询 + 逐行 json.loads)
122
+ 耗时显著; pickle.load 直接读回对象快数倍。code-keyword 实测 180ms→37ms。
90
123
  缓存失效: kg.duckdb 的 mtime 变了就重建 (和 context_pack 缓存逻辑一致)。
91
124
  降级: pickle 损坏/版本不兼容 → 静默回退 DuckDB 重建, 不报错。
125
+ 线程/进程安全: 只读连接共享, pickle 写用 temp+replace (原子), 失败不影响返回。
126
+
127
+ Args:
128
+ con: DuckDB 只读连接。
129
+ cache_name: 缓存文件名 (如 'code-keyword' → .runtime/code-keyword.pkl)。
130
+ loader_fn: 接收 con, 返回重建后的对象; 返回 None / 空视为无数据。
131
+ empty_sentinel: loader 返回此值时也视为无数据、不缓存 (默认 None)。
92
132
  """
93
133
  import pickle
94
134
  duckdb_path = os.path.join(INDEX_DIR, 'kg.duckdb')
95
- cache_path = os.path.join(BASE, '.qoder', '.runtime', 'code-keyword.pkl')
135
+ cache_path = os.path.join(BASE, '.qoder', '.runtime', cache_name + '.pkl')
96
136
  db_mtime = os.path.getmtime(duckdb_path) if os.path.isfile(duckdb_path) else 0
97
137
 
98
138
  # ① 缓存有效 (存在 + mtime 匹配) → 直接 load, 跳过 DuckDB
@@ -105,18 +145,17 @@ def _load_keyword_with_cache(con):
105
145
  pass # 缓存损坏 → 回退重建
106
146
 
107
147
  # ② 缓存无效/缺失 → 从 DuckDB 重建
108
- rows = con.execute("SELECT keyword, file_path FROM keywords").fetchall()
109
- result = {}
110
- for kw, fp in rows:
111
- result.setdefault(kw, []).append(fp)
112
- if not result:
113
- return None
148
+ result = loader_fn(con)
149
+ if result is None or result == empty_sentinel:
150
+ return None # 无数据, 不缓存 (避免缓存空结果误导后续)
114
151
 
115
- # ③ 写缓存 (best-effort, 失败不影响返回结果)
152
+ # ③ 写缓存 (best-effort, 失败不影响返回结果; 用原子写防止半文件)
116
153
  try:
117
154
  os.makedirs(os.path.dirname(cache_path), exist_ok=True)
118
- with open(cache_path, 'wb') as f:
155
+ tmp = cache_path + '.tmp'
156
+ with open(tmp, 'wb') as f:
119
157
  pickle.dump(result, f, protocol=pickle.HIGHEST_PROTOCOL)
158
+ os.replace(tmp, cache_path)
120
159
  except OSError:
121
160
  pass
122
161
  return result
@@ -135,58 +174,75 @@ def _duckdb_load(filename):
135
174
  try:
136
175
  if filename == 'code-keyword.json':
137
176
  # {keyword: [files]} — 11836 行重建耗时 180ms, 用 pickle 缓存提速到 37ms
138
- return _load_keyword_with_cache(con)
177
+ def _kw_loader(con):
178
+ rows = con.execute("SELECT keyword, file_path FROM keywords").fetchall()
179
+ result = {}
180
+ for kw, fp in rows:
181
+ result.setdefault(kw, []).append(fp)
182
+ return result if result else None
183
+ return _load_with_pickle_cache(con, 'code-keyword', _kw_loader)
139
184
 
140
185
  elif filename == 'code-api.json':
141
186
  # {endpoint: controller_file}
142
- rows = con.execute("SELECT endpoint, controller_file FROM endpoints").fetchall()
143
- return dict(rows) if rows else None
187
+ def _api_loader(con):
188
+ rows = con.execute("SELECT endpoint, controller_file FROM endpoints").fetchall()
189
+ return dict(rows) if rows else None
190
+ return _load_with_pickle_cache(con, 'code-api', _api_loader)
144
191
 
145
192
  elif filename == 'ui-style.json':
146
193
  # Reconstruct the full ui-style structure from pages/fields/style_meta
147
- result = {'version': '1.0', 'projects': {}, 'common_components': {}, 'form_components': {}, 'field_map': {}}
148
- # Pages
149
- page_rows = con.execute("SELECT file, project, page_type, components, table_columns, form_fields, layout FROM pages").fetchall()
150
- for f, proj, ptype, comps, cols, fields, layout in page_rows:
151
- proj_data = result['projects'].setdefault(proj, {'page_types': {}, 'page_examples': {}})
152
- proj_data['page_types'][ptype] = proj_data['page_types'].get(ptype, 0) + 1
153
- examples = proj_data['page_examples'].setdefault(ptype, [])
154
- examples.append({'file': f, 'components': json.loads(comps) if comps else [],
155
- 'table_columns': json.loads(cols) if cols else [],
156
- 'form_fields': json.loads(fields) if fields else [],
157
- 'layout': json.loads(layout) if layout else {}})
158
- # Fields
159
- field_rows = con.execute("SELECT field, title, count, sample_files FROM fields").fetchall()
160
- for field, title, count, samples in field_rows:
161
- titles = title.split('/') if title else []
162
- result['field_map'][field] = {'field': field, 'titles': titles, 'count': count,
163
- 'sample_files': json.loads(samples) if samples else []}
164
- # Style meta (layout_fingerprint per project)
165
- meta_rows = con.execute("SELECT project, stack, layout, access_mode, sidebar_width, color_primary, color_success, color_warning, color_destructive, radius, vue_files, common_components, form_components FROM style_meta").fetchall()
166
- for row in meta_rows:
167
- proj = row[0]
168
- proj_data = result['projects'].setdefault(proj, {'page_types': {}, 'page_examples': {}})
169
- proj_data['layout_fingerprint'] = {
170
- 'stack': row[1], 'layout': row[2], 'access_mode': row[3],
171
- 'sidebar_width': row[4], 'color_primary': row[5], 'color_success': row[6],
172
- 'color_warning': row[7], 'color_destructive': row[8], 'radius': row[9],
173
- }
174
- proj_data['vue_files'] = row[10]
175
- if row[11]:
176
- result['common_components'] = json.loads(row[11])
177
- if row[12]:
178
- result['form_components'] = json.loads(row[12])
179
- return result if result['projects'] or result['field_map'] else None
194
+ # 最贵的索引: 3 DuckDB 查询 + components/table_columns/
195
+ # form_fields/layout 逐行 json.loads。加 pickle 缓存后二次调用秒回。
196
+ def _style_loader(con):
197
+ result = {'version': '1.0', 'projects': {}, 'common_components': {}, 'form_components': {}, 'field_map': {}}
198
+ # Pages
199
+ page_rows = con.execute("SELECT file, project, page_type, components, table_columns, form_fields, layout FROM pages").fetchall()
200
+ for f, proj, ptype, comps, cols, fields, layout in page_rows:
201
+ proj_data = result['projects'].setdefault(proj, {'page_types': {}, 'page_examples': {}})
202
+ proj_data['page_types'][ptype] = proj_data['page_types'].get(ptype, 0) + 1
203
+ examples = proj_data['page_examples'].setdefault(ptype, [])
204
+ examples.append({'file': f, 'components': json.loads(comps) if comps else [],
205
+ 'table_columns': json.loads(cols) if cols else [],
206
+ 'form_fields': json.loads(fields) if fields else [],
207
+ 'layout': json.loads(layout) if layout else {}})
208
+ # Fields
209
+ field_rows = con.execute("SELECT field, title, count, sample_files FROM fields").fetchall()
210
+ for field, title, count, samples in field_rows:
211
+ titles = title.split('/') if title else []
212
+ result['field_map'][field] = {'field': field, 'titles': titles, 'count': count,
213
+ 'sample_files': json.loads(samples) if samples else []}
214
+ # Style meta (layout_fingerprint per project)
215
+ meta_rows = con.execute("SELECT project, stack, layout, access_mode, sidebar_width, color_primary, color_success, color_warning, color_destructive, radius, vue_files, common_components, form_components FROM style_meta").fetchall()
216
+ for row in meta_rows:
217
+ proj = row[0]
218
+ proj_data = result['projects'].setdefault(proj, {'page_types': {}, 'page_examples': {}})
219
+ proj_data['layout_fingerprint'] = {
220
+ 'stack': row[1], 'layout': row[2], 'access_mode': row[3],
221
+ 'sidebar_width': row[4], 'color_primary': row[5], 'color_success': row[6],
222
+ 'color_warning': row[7], 'color_destructive': row[8], 'radius': row[9],
223
+ }
224
+ proj_data['vue_files'] = row[10]
225
+ if row[11]:
226
+ result['common_components'] = json.loads(row[11])
227
+ if row[12]:
228
+ result['form_components'] = json.loads(row[12])
229
+ return result if result['projects'] or result['field_map'] else None
230
+ # empty_sentinel: 全空结构 (version 是固定键, 不算数据)
231
+ return _load_with_pickle_cache(
232
+ con, 'ui-style', _style_loader,
233
+ empty_sentinel={'version': '1.0', 'projects': {}, 'common_components': {}, 'form_components': {}, 'field_map': {}})
180
234
 
181
235
  elif filename == 'prd-index.json':
182
- rows = con.execute("SELECT file, title, keywords, features, cn_terms FROM prds").fetchall()
183
- result = {}
184
- for f, title, kws, feats, cn in rows:
185
- result[f] = {'title': title,
186
- 'keywords': json.loads(kws) if kws else [],
187
- 'features': json.loads(feats) if feats else [],
188
- 'cn_terms': json.loads(cn) if cn else []}
189
- return result if result else None
236
+ def _prd_loader(con):
237
+ rows = con.execute("SELECT file, title, keywords, features, cn_terms FROM prds").fetchall()
238
+ result = {}
239
+ for f, title, kws, feats, cn in rows:
240
+ result[f] = {'title': title,
241
+ 'keywords': json.loads(kws) if kws else [],
242
+ 'features': json.loads(feats) if feats else [],
243
+ 'cn_terms': json.loads(cn) if cn else []}
244
+ return result if result else None
245
+ return _load_with_pickle_cache(con, 'prd-index', _prd_loader)
190
246
 
191
247
  elif filename == 'ui-modules.json':
192
248
  rows = con.execute("SELECT dir, cn, project, path FROM modules").fetchall()
@@ -227,11 +283,24 @@ def _match_keyword(query_word, kw):
227
283
 
228
284
 
229
285
  def _search_cache_key(query, platform):
230
- """缓存 key: query|platform|keyword-index mtime。"""
286
+ """缓存 key: query|platform|数据源 mtime 指纹。
287
+
288
+ 数据真正存在 DuckDB(kg.duckdb), code-keyword.json 在 DuckDB 模式下不存在。
289
+ 若只用 JSON 的 mtime 做指纹, DuckDB 模式下恒为 '0' —— DuckDB 增量更新后,
290
+ 旧的搜索缓存仍会命中, 返回陈旧结果(与 context_pack 同源 bug, 那边已修)。
291
+ 所以指纹必须以 kg.duckdb mtime 为主, JSON 为辅。
292
+ """
231
293
  import hashlib
232
- ki_path = os.path.join(BASE, 'data', 'index', 'code-keyword.json') if os.path.isdir(os.path.join(BASE, 'data', 'index')) else None
233
- mtime = '%d' % os.path.getmtime(ki_path) if ki_path and os.path.isfile(ki_path) else '0'
234
- raw = '{}|{}|{}'.format(query.lower(), platform or '', mtime)
294
+ mtime_str = ''
295
+ # DuckDB (数据真正来源, 必须算)
296
+ duckdb_path = os.path.join(BASE, 'data', 'index', 'kg.duckdb')
297
+ if os.path.isfile(duckdb_path):
298
+ mtime_str += 'duck:%d_' % os.path.getmtime(duckdb_path)
299
+ # ② JSON 索引 (DuckDB 缺失时的降级数据源, 一起算)
300
+ ki_path = os.path.join(BASE, 'data', 'index', 'code-keyword.json')
301
+ if os.path.isfile(ki_path):
302
+ mtime_str += 'kw:%d' % os.path.getmtime(ki_path)
303
+ raw = '{}|{}|{}'.format(query.lower(), platform or '', mtime_str or '0')
235
304
  return hashlib.md5(raw.encode('utf-8')).hexdigest()[:16]
236
305
 
237
306
 
@@ -297,7 +366,7 @@ def _check_index_freshness():
297
366
  if warn_mtime > meta_mtime:
298
367
  return # 已经提示过了 (在本次索引周期内)
299
368
  print('[提示] 代码有更新但知识图谱未刷新 (上次索引: %s)' % last_sync_str)
300
- print(' 刷新: python .qoder/scripts/git_sync.py --index-only')
369
+ print(' 刷新: python .qoder/scripts/domain/task/git_sync.py --index-only')
301
370
  try:
302
371
  os.makedirs(os.path.dirname(warned_path), exist_ok=True)
303
372
  open(warned_path, 'w').close()
@@ -308,10 +377,9 @@ def _check_index_freshness():
308
377
 
309
378
 
310
379
  def search_keywords(query, platform=None):
311
- # 新鲜度检查 (代码改了但索引没更新 提示, 不阻塞)
312
- _check_index_freshness()
313
-
314
- # 结果缓存: 同 query+platform+索引未变 → 直接返回缓存输出
380
+ # 先查缓存 (命中即数据未变, 直接还)。缓存 key 已含 kg.duckdb mtime, 命中就说明
381
+ # 数据源没变, 跳过 _check_index_freshness 的 os.walk —— 50 人团队每天几十次
382
+ # 搜索, 每次都 walk 目录是纯浪费。未命中才做新鲜度检查 + 计算。
315
383
  cache_key = _search_cache_key(query, platform)
316
384
  cache_path = os.path.join(BASE, '.qoder', '.runtime', 'search-cache-%s.txt' % cache_key)
317
385
  if os.path.isfile(cache_path):
@@ -324,6 +392,9 @@ def search_keywords(query, platform=None):
324
392
  except OSError:
325
393
  pass
326
394
 
395
+ # 缓存未命中 → 才做新鲜度检查 (代码改了但索引没更新 → 提示, 不阻塞)
396
+ _check_index_freshness()
397
+
327
398
  # 缓存未命中, 正常计算并写入缓存
328
399
  import io
329
400
  old_stdout = sys.stdout
@@ -373,12 +444,12 @@ def _cleanup_cache_dir(cache_dir, prefix, max_files=50):
373
444
 
374
445
 
375
446
  # _expand_chinese_query 已移至 common/terms.py (单一数据源, context_pack 共用)
376
- # import 见文件头部: from common.terms import expand_chinese_query
447
+ # import 见文件头部: from foundation.integrations.terms import expand_chinese_query
377
448
 
378
449
 
379
450
  def _search_keywords_impl(query, platform=None):
380
451
  """实际的搜索逻辑 (原 search_keywords 内容)。"""
381
- ki = load_index('code-keyword.json', hint='Run: python git_sync.py')
452
+ ki = load_index('code-keyword.json', hint='Run: python .qoder/scripts/domain/task/git_sync.py')
382
453
  if ki is None:
383
454
  return
384
455
 
@@ -395,8 +466,8 @@ def _search_keywords_impl(query, platform=None):
395
466
 
396
467
  # 性能优化 A2: 用倒排索引缩小候选集 (从 O(all_keys) 降到 O(命中数))
397
468
  try:
398
- from common.search_engine import get_inverted_index, search_keywords_fast
399
- from common.paths import get_repo_root
469
+ from search_engine import get_inverted_index, search_keywords_fast
470
+ from foundation.core.paths import get_repo_root
400
471
  inverted = get_inverted_index(str(get_repo_root()))
401
472
  kw_matches = search_keywords_fast(words, inverted, ki)
402
473
  except Exception:
@@ -418,6 +489,7 @@ def _search_keywords_impl(query, platform=None):
418
489
 
419
490
  if not matches:
420
491
  print('No matches for: {}'.format(query))
492
+ _semantic_fallback(query, platform)
421
493
  return
422
494
 
423
495
  total = 0
@@ -436,8 +508,44 @@ def _search_keywords_impl(query, platform=None):
436
508
  print('Total: {} matches'.format(total))
437
509
 
438
510
 
511
+ def _semantic_fallback(query, platform=None):
512
+ """关键词 0 命中时, 用语义检索 (bge-small-zh embedding) 补一轮。
513
+
514
+ 解决"中文查英文代码"的漏召: 用户查"报销"时关键词匹配不到 reimburse/expense,
515
+ 但语义向量能跨语言命中。把命中的实体解析成文件, 按 platform 过滤后列出。
516
+
517
+ 模型未装/无 embedding → 静默跳过, 不报错 (符合优雅降级原则)。
518
+ """
519
+ try:
520
+ from domain.kg.graph.kg_semantic import semantic_search_files
521
+ except ImportError:
522
+ return
523
+ results = semantic_search_files(query, top_k=8)
524
+ if not results:
525
+ return
526
+ # 按 platform 过滤 + 补全项目前缀 (相对 vue 路径用存在性兜底)
527
+ filtered = []
528
+ try:
529
+ from domain.kg.graph.kg_semantic import filter_semantic_by_platform
530
+ filtered = filter_semantic_by_platform(results, platform, BASE)
531
+ except Exception:
532
+ filtered = [(fp.replace('\\', '/'), cn, sc) for fp, eid, cn, sc in results]
533
+ if not filtered:
534
+ return
535
+ print()
536
+ print('[语义补充] 关键词未命中, 用语义检索找到 (中→英跨语言):')
537
+ for fp, cn, score in filtered[:6]:
538
+ short = fp
539
+ if len(short.split('/')) > 5:
540
+ parts = short.split('/')
541
+ short = '/'.join(parts[:2]) + '/.../' + '/'.join(parts[-2:])
542
+ full = 'data/code/' + fp
543
+ print(' %.2f %s -> %s' % (score, cn[:20], full))
544
+
545
+
546
+
439
547
  def search_prds(query):
440
- prd_index = load_index('prd-index.json', hint='Run: python git_sync.py --prd-only')
548
+ prd_index = load_index('prd-index.json', hint='Run: python .qoder/scripts/domain/task/git_sync.py --prd-only')
441
549
  if prd_index is None:
442
550
  return
443
551
  q = query.lower()
@@ -538,7 +646,7 @@ def search_field(field_name):
538
646
 
539
647
  def search_api(query):
540
648
  """Search API endpoints in code-api.json."""
541
- ai = load_index('code-api.json', hint='Run: python git_sync.py --index-only')
649
+ ai = load_index('code-api.json', hint='Run: python .qoder/scripts/domain/task/git_sync.py --index-only')
542
650
  if ai is None:
543
651
  return
544
652
  q = query.lower()
@@ -555,7 +663,7 @@ def search_api(query):
555
663
 
556
664
  def show_modules():
557
665
  """Show project overview from code-stats.json."""
558
- mm = load_index('code-stats.json', hint='Run: python git_sync.py --index-only')
666
+ mm = load_index('code-stats.json', hint='Run: python .qoder/scripts/domain/task/git_sync.py --index-only')
559
667
  if mm is None:
560
668
  return
561
669
  print('=== Project Modules ===')
@@ -569,7 +677,7 @@ def show_modules():
569
677
 
570
678
  def show_top_keywords():
571
679
  """Show top keywords by file count."""
572
- ki = load_index('code-keyword.json', hint='Run: python git_sync.py')
680
+ ki = load_index('code-keyword.json', hint='Run: python .qoder/scripts/domain/task/git_sync.py')
573
681
  if ki is None:
574
682
  return
575
683
  top = sorted(ki.items(), key=lambda x: len(x[1]), reverse=True)[:50]
@@ -580,20 +688,33 @@ def show_top_keywords():
580
688
 
581
689
 
582
690
  def show_components():
583
- meta = load_index('ui-style-meta.json', hint='Run: python build_style_index.py')
584
- if meta is None:
691
+ # 组件统计存在 DuckDB style_meta.common_components/form_components (JSON ),
692
+ # kg_build -> kg_duckdb 从 ui-style.json 导入。
693
+ # 注意: 不是独立 JSON 文件, 走 DuckDB 路径。
694
+ si = load_index('ui-style.json')
695
+ if si is None:
696
+ print('UI 样式索引未构建。组件统计在 ui-style.json → kg.duckdb.style_meta 表。')
697
+ print('构建: npx @hupan56/wlkj run kg_build.py --rebuild (管理员)')
698
+ print(' 非 admin: npx @hupan56/wlkj doctor (拉团队已构建的 kg.duckdb)')
699
+ return
700
+ common = si.get('common_components', {})
701
+ forms = si.get('form_components', {})
702
+ if not common and not forms:
703
+ print('style_meta 表的组件列为空 — kg_duckdb 导入时 ui-style.json 缺 common_components。')
704
+ print('重建: npx @hupan56/wlkj run build_style_index.py 然后 kg_build.py --rebuild')
585
705
  return
586
706
  print('=== UI Components Usage ===')
587
707
  print()
588
708
  print('Ant Design Vue (import counts):')
589
- for comp, count in meta.get('common_components', {}).items():
709
+ for comp, count in common.items():
590
710
  print(' {} {}'.format(comp.ljust(20), str(count).ljust(5)))
591
711
  print()
592
712
  print('Form Components (usage in schemas):')
593
- for comp, count in sorted(meta.get('form_components', {}).items(), key=lambda x: x[1], reverse=True)[:20]:
713
+ for comp, count in sorted(forms.items(), key=lambda x: x[1], reverse=True)[:20]:
594
714
  print(' {} {}'.format(comp.ljust(20), str(count).ljust(5)))
595
715
 
596
716
 
717
+
597
718
  def show_vben():
598
719
  data = load_index('ref-vben-style.json')
599
720
  if data is None:
@@ -611,8 +732,8 @@ def show_vben():
611
732
  def search_wiki_cli(query):
612
733
  """搜 Repo Wiki (Qoder IDE 生成的语义级模块文档)。"""
613
734
  try:
614
- from common.repowiki import search_wiki, build_wiki_index
615
- from common.terms import get_cn_map_with_auto
735
+ from repowiki import search_wiki, build_wiki_index
736
+ from foundation.integrations.terms import get_cn_map_with_auto
616
737
  except ImportError:
617
738
  print('Wiki 搜索不可用 (common.repowiki 模块缺失)')
618
739
  return
@@ -657,7 +778,7 @@ def search_wiki_cli(query):
657
778
 
658
779
  def search_impact(query):
659
780
  """改这个接口/符号影响哪些页面/按钮/模块? (U1: 内存图遍历)"""
660
- from common.graph_traverse import CodeGraph
781
+ from domain.kg.graph.graph_traverse import CodeGraph
661
782
  g = CodeGraph(INDEX_DIR)
662
783
  result = g.impact(query)
663
784
  if result:
@@ -669,7 +790,7 @@ def search_impact(query):
669
790
 
670
791
  def search_context360(query):
671
792
  """符号的360度视图: 端点/函数/处理器/PRD (U1: 内存图遍历)"""
672
- from common.graph_traverse import CodeGraph
793
+ from domain.kg.graph.graph_traverse import CodeGraph
673
794
  g = CodeGraph(INDEX_DIR)
674
795
  result = g.context360(query)
675
796
  if result:
@@ -754,8 +875,9 @@ def _graph_cli(method, *args):
754
875
  """
755
876
  try:
756
877
  if method == 'get_workflow':
757
- # DuckDB 路径
758
- import kg_duckdb as K
878
+ # DuckDB 路径 (v3.0: kg_duckdb 已迁入 kg/ 子包)
879
+ from domain.kg.storage.kg_duckdb import get_db as _kg_get_db
880
+ import domain.kg.storage.kg_duckdb as K
759
881
  q = (args[0] if args else '').strip()
760
882
  if not q:
761
883
  print('请提供模块名, 如: assets / cost / hr / maintenance')
@@ -774,8 +896,8 @@ def _graph_cli(method, *args):
774
896
  print(' %d. [%s] %s — %s' % (r[0], r[1] or '?', r[2] or '', r[3] or ''))
775
897
  return
776
898
  # JSON 索引路径 (CodeGraph)
777
- from common.graph_traverse import CodeGraph
778
- from common.paths import DATA_INDEX_DIR
899
+ from domain.kg.graph.graph_traverse import CodeGraph
900
+ from foundation.core.paths import DATA_INDEX_DIR
779
901
  g = CodeGraph(str(DATA_INDEX_DIR))
780
902
  fn = getattr(g, method, None)
781
903
  if not fn: