@hupan56/wlkj 2.6.0 → 2.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/bin/cli.js +269 -11
  2. package/package.json +1 -1
  3. package/templates/qoder/agents/insight-planning.md +67 -0
  4. package/templates/qoder/agents/insight-research.md +61 -0
  5. package/templates/qoder/agents/prd-reference.md +8 -2
  6. package/templates/qoder/commands/optional/wl-insight.md +275 -0
  7. package/templates/qoder/commands/{wl-report.md → optional/wl-report.md} +13 -5
  8. package/templates/qoder/commands/{wl-spec.md → optional/wl-spec.md} +1 -1
  9. package/templates/qoder/commands/{wl-status.md → optional/wl-status.md} +28 -2
  10. package/templates/qoder/commands/wl-code.md +10 -2
  11. package/templates/qoder/commands/wl-commit.md +1 -1
  12. package/templates/qoder/commands/wl-design-draw.md +78 -0
  13. package/templates/qoder/commands/wl-design-scan.md +108 -0
  14. package/templates/qoder/commands/wl-design-spec.md +154 -0
  15. package/templates/qoder/commands/wl-design.md +32 -0
  16. package/templates/qoder/commands/wl-init.md +24 -3
  17. package/templates/qoder/commands/wl-prd-full.md +226 -0
  18. package/templates/qoder/commands/wl-prd-quick.md +134 -0
  19. package/templates/qoder/commands/wl-prd-review.md +104 -0
  20. package/templates/qoder/commands/wl-prd.md +17 -311
  21. package/templates/qoder/commands/wl-search.md +66 -30
  22. package/templates/qoder/commands/wl-task.md +290 -59
  23. package/templates/qoder/commands/wl-test.md +92 -24
  24. package/templates/qoder/config.yaml +51 -15
  25. package/templates/qoder/hooks/inject-workflow-state.py +2 -2
  26. package/templates/qoder/hooks/session-start.py +82 -51
  27. package/templates/qoder/rules/wl-pipeline.md +216 -105
  28. package/templates/qoder/scripts/__pycache__/search_index.cpython-39.pyc +0 -0
  29. package/templates/qoder/scripts/archive_prd.py +377 -0
  30. package/templates/qoder/scripts/autotest.py +1715 -0
  31. package/templates/qoder/scripts/autotest_batch.py +224 -0
  32. package/templates/qoder/scripts/autotest_run.py +297 -0
  33. package/templates/qoder/scripts/benchmark.py +210 -209
  34. package/templates/qoder/scripts/build_style_index.py +444 -4
  35. package/templates/qoder/scripts/check_carriers.py +238 -0
  36. package/templates/qoder/scripts/check_mcp.py +298 -0
  37. package/templates/qoder/scripts/check_qoderwork_consistency.py +166 -0
  38. package/templates/qoder/scripts/common/events.py +46 -0
  39. package/templates/qoder/scripts/common/extract.py +419 -0
  40. package/templates/qoder/scripts/common/graph_traverse.py +533 -0
  41. package/templates/qoder/scripts/common/paths.py +89 -0
  42. package/templates/qoder/scripts/common/pip_install.py +144 -0
  43. package/templates/qoder/scripts/common/platform_guard.py +61 -0
  44. package/templates/qoder/scripts/common/search_engine.py +205 -205
  45. package/templates/qoder/scripts/common/terms.py +57 -0
  46. package/templates/qoder/scripts/common/ts_extract.py +536 -0
  47. package/templates/qoder/scripts/context_pack.py +73 -13
  48. package/templates/qoder/scripts/enrich_prompt.py +226 -0
  49. package/templates/qoder/scripts/eval_prd.py +318 -231
  50. package/templates/qoder/scripts/export.py +487 -487
  51. package/templates/qoder/scripts/extract_api_params.py +246 -0
  52. package/templates/qoder/scripts/extract_routes.py +54 -0
  53. package/templates/qoder/scripts/extract_routes_tree.py +78 -0
  54. package/templates/qoder/scripts/fill_prototype.py +707 -0
  55. package/templates/qoder/scripts/gen_design_doc.py +394 -0
  56. package/templates/qoder/scripts/git_sync.py +27 -15
  57. package/templates/qoder/scripts/init_doctor.py +292 -40
  58. package/templates/qoder/scripts/install_qoderwork.py +366 -9
  59. package/templates/qoder/scripts/kg.py +708 -0
  60. package/templates/qoder/scripts/kg_auto_login.py +196 -0
  61. package/templates/qoder/scripts/kg_build.py +612 -0
  62. package/templates/qoder/scripts/kg_build_db.py +327 -0
  63. package/templates/qoder/scripts/kg_duckdb.py +549 -0
  64. package/templates/qoder/scripts/kg_incremental.py +393 -0
  65. package/templates/qoder/scripts/kg_link_db.py +224 -0
  66. package/templates/qoder/scripts/kg_mcp_server.py +801 -0
  67. package/templates/qoder/scripts/kg_semantic.py +150 -0
  68. package/templates/qoder/scripts/kg_test_runner.py +241 -0
  69. package/templates/qoder/scripts/lanhu_stdio_wrapper.py +119 -0
  70. package/templates/qoder/scripts/learn.py +118 -39
  71. package/templates/qoder/scripts/learn_aggregate.py +201 -0
  72. package/templates/qoder/scripts/mcp_launcher.py +359 -0
  73. package/templates/qoder/scripts/mysql_mcp_server.py +396 -0
  74. package/templates/qoder/scripts/repo_root.py +106 -0
  75. package/templates/qoder/scripts/role.py +12 -0
  76. package/templates/qoder/scripts/run_weekly_update.bat +5 -0
  77. package/templates/qoder/scripts/run_weekly_update.sh +5 -0
  78. package/templates/qoder/scripts/search_index.py +307 -60
  79. package/templates/qoder/scripts/secure-ls.js +5640 -0
  80. package/templates/qoder/scripts/setup.py +706 -641
  81. package/templates/qoder/scripts/setup_lanhu.py +963 -0
  82. package/templates/qoder/scripts/status.py +250 -11
  83. package/templates/qoder/scripts/sync_carriers.py +259 -0
  84. package/templates/qoder/scripts/syncgate.py +2 -2
  85. package/templates/qoder/scripts/task.py +75 -0
  86. package/templates/qoder/scripts/team_sync.py +60 -4
  87. package/templates/qoder/scripts/workspace_init.py +1 -1
  88. package/templates/qoder/skills/design-import/SKILL.md +226 -0
  89. package/templates/qoder/skills/design-import/figma-workflow.md +81 -0
  90. package/templates/qoder/skills/design-review/SKILL.md +82 -25
  91. package/templates/qoder/skills/prd-generator/SKILL.md +184 -60
  92. package/templates/qoder/skills/prd-review/SKILL.md +18 -1
  93. package/templates/qoder/skills/prompt-enrich/SKILL.md +90 -0
  94. package/templates/qoder/skills/prototype-generator/SKILL.md +256 -141
  95. package/templates/qoder/skills/prototype-generator/SKILL.md.zcode-79180-2af4721f-f9a6-412c-88db-c0af680d211b.tmp +0 -0
  96. package/templates/qoder/skills/spec-coder/SKILL.md +18 -1
  97. package/templates/qoder/skills/spec-generator/SKILL.md +18 -1
  98. package/templates/qoder/skills/test-generator/SKILL.md +15 -2
  99. package/templates/qoder/skills/wl-code/SKILL.md +55 -36
  100. package/templates/qoder/skills/wl-commit/SKILL.md +89 -76
  101. package/templates/qoder/skills/wl-design/SKILL.md +55 -0
  102. package/templates/qoder/skills/wl-init/SKILL.md +76 -67
  103. package/templates/qoder/skills/wl-insight/SKILL.md +201 -81
  104. package/templates/qoder/skills/wl-prd-full/SKILL.md +69 -0
  105. package/templates/qoder/skills/wl-prd-quick/SKILL.md +49 -0
  106. package/templates/qoder/skills/wl-prd-review/SKILL.md +34 -0
  107. package/templates/qoder/skills/wl-report/SKILL.md +131 -107
  108. package/templates/qoder/skills/wl-search/SKILL.md +141 -75
  109. package/templates/qoder/skills/wl-spec/SKILL.md +49 -39
  110. package/templates/qoder/skills/wl-status/SKILL.md +83 -61
  111. package/templates/qoder/skills/wl-task/SKILL.md +132 -58
  112. package/templates/qoder/skills/wl-test/SKILL.md +406 -40
  113. package/templates/qoder/templates/prototype-app.html +13 -8
  114. package/templates/qoder/templates/prototype-web.html +376 -93
  115. package/templates/root/AGENTS.md +89 -34
  116. package/templates/root/requirements.txt +21 -0
  117. package/templates/root//344/275/277/347/224/250/350/257/264/346/230/216.md +259 -259
  118. package/templates/root//346/226/260/346/211/213/346/214/207/345/215/227.md +186 -186
  119. package/templates/qoder/agents/prd-planning.md +0 -57
  120. package/templates/qoder/agents/prd-research.md +0 -34
  121. package/templates/qoder/commands/wl-insight.md +0 -51
  122. package/templates/qoder/scripts/__pycache__/init_doctor.cpython-39.pyc +0 -0
  123. package/templates/qoder/scripts/__pycache__/setup.cpython-39.pyc +0 -0
  124. package/templates/qoder/scripts/common/__pycache__/developer.cpython-39.pyc +0 -0
  125. package/templates/qoder/skills/wl-prd/SKILL.md +0 -92
@@ -27,7 +27,7 @@ except (AttributeError, TypeError, OSError, IOError):
27
27
  pass
28
28
 
29
29
  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
30
+ from common.terms import CN_MAP, BUSINESS_PATH_MAP, CN_TO_EN, get_platform_map, expand_chinese_query
31
31
  PLATFORM_MAP = get_platform_map()
32
32
 
33
33
  BASE = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
@@ -38,7 +38,20 @@ MIN_FUZZY_LEN = 4
38
38
 
39
39
 
40
40
  def load_index(filename, hint=''):
41
+ """Load an index. Prefers DuckDB (kg.duckdb), falls back to JSON file.
42
+
43
+ Returns the same dict structure as the JSON file would have, so all
44
+ callers work unchanged regardless of the backend.
45
+ """
46
+ # Try DuckDB first
47
+ result = _duckdb_load(filename)
48
+ if result is not None:
49
+ return result
50
+
51
+ # Fallback: read JSON file
41
52
  path = os.path.join(INDEX_DIR, filename)
53
+ if not os.path.isfile(path):
54
+ path = os.path.join(INDEX_DIR, '_build_cache', filename)
42
55
  if not os.path.isfile(path):
43
56
  print('{} not found.{}'.format(filename, ' ' + hint if hint else ''))
44
57
  return None
@@ -50,6 +63,150 @@ def load_index(filename, hint=''):
50
63
  return None
51
64
 
52
65
 
66
+ # DuckDB path (lazy-connect)
67
+ _duckdb_con = None
68
+
69
+
70
+ def _get_duckdb():
71
+ global _duckdb_con
72
+ if _duckdb_con is not None:
73
+ return _duckdb_con
74
+ db_path = os.path.join(INDEX_DIR, 'kg.duckdb')
75
+ if not os.path.isfile(db_path):
76
+ return None
77
+ try:
78
+ import duckdb
79
+ _duckdb_con = duckdb.connect(db_path, read_only=True)
80
+ return _duckdb_con
81
+ except Exception:
82
+ return None
83
+
84
+
85
+ def _load_keyword_with_cache(con):
86
+ """加载 code-keyword (11836 行), 带 pickle 缓存层。
87
+
88
+ 性能: DuckDB SELECT + Python 循环重建 = ~180ms;
89
+ pickle.load = ~37ms (提速 5x, 实测数据一致)。
90
+ 缓存失效: kg.duckdb 的 mtime 变了就重建 (和 context_pack 缓存逻辑一致)。
91
+ 降级: pickle 损坏/版本不兼容 → 静默回退 DuckDB 重建, 不报错。
92
+ """
93
+ import pickle
94
+ duckdb_path = os.path.join(INDEX_DIR, 'kg.duckdb')
95
+ cache_path = os.path.join(BASE, '.qoder', '.runtime', 'code-keyword.pkl')
96
+ db_mtime = os.path.getmtime(duckdb_path) if os.path.isfile(duckdb_path) else 0
97
+
98
+ # ① 缓存有效 (存在 + mtime 匹配) → 直接 load, 跳过 DuckDB
99
+ if os.path.isfile(cache_path):
100
+ try:
101
+ if os.path.getmtime(cache_path) >= db_mtime:
102
+ with open(cache_path, 'rb') as f:
103
+ return pickle.load(f)
104
+ except (pickle.PickleError, EOFError, OSError):
105
+ pass # 缓存损坏 → 回退重建
106
+
107
+ # ② 缓存无效/缺失 → 从 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
114
+
115
+ # ③ 写缓存 (best-effort, 失败不影响返回结果)
116
+ try:
117
+ os.makedirs(os.path.dirname(cache_path), exist_ok=True)
118
+ with open(cache_path, 'wb') as f:
119
+ pickle.dump(result, f, protocol=pickle.HIGHEST_PROTOCOL)
120
+ except OSError:
121
+ pass
122
+ return result
123
+
124
+
125
+ def _duckdb_load(filename):
126
+ """Load an index from DuckDB, returning the same structure as the JSON.
127
+
128
+ Returns None if DuckDB unavailable or table doesn't have data,
129
+ triggering the JSON fallback.
130
+ """
131
+ con = _get_duckdb()
132
+ if con is None:
133
+ return None
134
+
135
+ try:
136
+ if filename == 'code-keyword.json':
137
+ # {keyword: [files]} — 11836 行重建耗时 180ms, 用 pickle 缓存提速到 37ms
138
+ return _load_keyword_with_cache(con)
139
+
140
+ elif filename == 'code-api.json':
141
+ # {endpoint: controller_file}
142
+ rows = con.execute("SELECT endpoint, controller_file FROM endpoints").fetchall()
143
+ return dict(rows) if rows else None
144
+
145
+ elif filename == 'ui-style.json':
146
+ # 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
180
+
181
+ 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
190
+
191
+ elif filename == 'ui-modules.json':
192
+ rows = con.execute("SELECT dir, cn, project, path FROM modules").fetchall()
193
+ result = {'projects': {}}
194
+ for d, cn, proj, path in rows:
195
+ result['projects'].setdefault(proj, []).append({'dir': d, 'cn': cn, 'path': path})
196
+ return result if result['projects'] else None
197
+
198
+ elif filename == 'code-stats.json':
199
+ rows = con.execute("SELECT project, vue_files FROM style_meta").fetchall()
200
+ result = {}
201
+ for proj, vf in rows:
202
+ result[proj] = {'files': vf or 0, 'classes': 0, 'apis': 0}
203
+ return result if result else None
204
+
205
+ except Exception:
206
+ return None
207
+ return None
208
+
209
+
53
210
  def filter_by_platform(files, platform=None):
54
211
  if not platform:
55
212
  return files
@@ -72,7 +229,7 @@ def _match_keyword(query_word, kw):
72
229
  def _search_cache_key(query, platform):
73
230
  """缓存 key: query|platform|keyword-index mtime。"""
74
231
  import hashlib
75
- ki_path = os.path.join(BASE, 'data', 'index', 'keyword-index.json') if os.path.isdir(os.path.join(BASE, 'data', 'index')) else None
232
+ ki_path = os.path.join(BASE, 'data', 'index', 'code-keyword.json') if os.path.isdir(os.path.join(BASE, 'data', 'index')) else None
76
233
  mtime = '%d' % os.path.getmtime(ki_path) if ki_path and os.path.isfile(ki_path) else '0'
77
234
  raw = '{}|{}|{}'.format(query.lower(), platform or '', mtime)
78
235
  return hashlib.md5(raw.encode('utf-8')).hexdigest()[:16]
@@ -215,61 +372,18 @@ def _cleanup_cache_dir(cache_dir, prefix, max_files=50):
215
372
  pass
216
373
 
217
374
 
218
- def _expand_chinese_query(query):
219
- """把中文查询拆成英文搜索词。
220
-
221
- 三级匹配:
222
- 1. 精确匹配: "异常" 在 CN_MAP → 直接扩展
223
- 2. 分词匹配: "异常记录" → 拆成 "异常" + "记录", 各自扩展
224
- (贪心最长匹配: 从长到短扫 CN_MAP 的 key)
225
- 3. 兜底: 拆不出来的部分, 原样小写 (让英文子串索引做匹配)
226
-
227
- 同时合并 BUSINESS_PATH_MAP 的路径模式。
228
- """
229
- words = []
230
- # 合并 CN_MAP + PRD 自动扩展
231
- try:
232
- from common.terms import get_cn_map_with_auto
233
- cn_map = get_cn_map_with_auto()
234
- except ImportError:
235
- cn_map = CN_MAP
236
-
237
- # 1. 精确匹配
238
- if query in cn_map:
239
- words.extend(cn_map[query].split())
240
- if query in BUSINESS_PATH_MAP:
241
- words.extend(p.strip('/-').lower() for p in BUSINESS_PATH_MAP[query])
242
-
243
- # 2. 分词匹配 (查询比单个 CN_MAP key 长 → 需要拆词)
244
- if not words and len(query) >= 3:
245
- # 贪心最长匹配: 把 CN_MAP keys 按长度降序排, 逐个从 query 里找
246
- remaining = query
247
- matched_segments = []
248
- for cn in sorted(cn_map.keys(), key=len, reverse=True):
249
- if cn in remaining:
250
- matched_segments.append(cn)
251
- remaining = remaining.replace(cn, ' ')
252
- if matched_segments:
253
- for seg in matched_segments:
254
- words.extend(cn_map[seg].split())
255
- if seg in BUSINESS_PATH_MAP:
256
- words.extend(p.strip('/-').lower() for p in BUSINESS_PATH_MAP[seg])
257
- # 剩余的英文/数字片段也加进去 (兜底)
258
- for frag in remaining.split():
259
- if frag and frag.isascii():
260
- words.append(frag.lower())
261
-
262
- return words
375
+ # _expand_chinese_query 已移至 common/terms.py (单一数据源, context_pack 共用)
376
+ # import 见文件头部: from common.terms import expand_chinese_query
263
377
 
264
378
 
265
379
  def _search_keywords_impl(query, platform=None):
266
380
  """实际的搜索逻辑 (原 search_keywords 内容)。"""
267
- ki = load_index('keyword-index.json', hint='Run: python git_sync.py')
381
+ ki = load_index('code-keyword.json', hint='Run: python git_sync.py')
268
382
  if ki is None:
269
383
  return
270
384
 
271
385
  # Expand Chinese query via the shared term maps (same maps used at build time)
272
- words = _expand_chinese_query(query)
386
+ words = expand_chinese_query(query)
273
387
  if words:
274
388
  words = sorted(set(w.lower() for w in words))
275
389
  print('Chinese: {} -> English: {}'.format(query, ' '.join(words)))
@@ -370,7 +484,7 @@ def search_prds(query):
370
484
 
371
485
  def search_style(style_type, platform=None):
372
486
  """Find pages of a given UI type (table/form/detail/modal/dashboard)."""
373
- si = load_index('style-index.json', hint='Run: python build_style_index.py')
487
+ si = load_index('ui-style.json', hint='Run: python build_style_index.py')
374
488
  if si is None:
375
489
  return
376
490
  q = style_type.lower()
@@ -389,7 +503,9 @@ def search_style(style_type, platform=None):
389
503
  found_any = True
390
504
  for pt in hit_types:
391
505
  print('[{}] {}: {} pages'.format(proj, pt, page_types[pt]))
392
- for f in examples.get(pt, [])[:8]:
506
+ for item in examples.get(pt, [])[:8]:
507
+ # page_examples 元素可能是 dict (新格式, 含结构) 或 str (旧格式)
508
+ f = item['file'] if isinstance(item, dict) else item
393
509
  print(' ' + f)
394
510
  if not examples.get(pt):
395
511
  print(' (no examples stored - run: python build_style_index.py)')
@@ -403,8 +519,8 @@ def search_style(style_type, platform=None):
403
519
 
404
520
 
405
521
  def search_field(field_name):
406
- """Search field usage in style-index.json field_map."""
407
- si = load_index('style-index.json', hint='Run: python build_style_index.py')
522
+ """Search field usage in ui-style.json field_map."""
523
+ si = load_index('ui-style.json', hint='Run: python build_style_index.py')
408
524
  if si is None:
409
525
  return
410
526
  fm = si.get('field_map', {})
@@ -421,8 +537,8 @@ def search_field(field_name):
421
537
 
422
538
 
423
539
  def search_api(query):
424
- """Search API endpoints in api-index.json."""
425
- ai = load_index('api-index.json', hint='Run: python git_sync.py --index-only')
540
+ """Search API endpoints in code-api.json."""
541
+ ai = load_index('code-api.json', hint='Run: python git_sync.py --index-only')
426
542
  if ai is None:
427
543
  return
428
544
  q = query.lower()
@@ -438,8 +554,8 @@ def search_api(query):
438
554
 
439
555
 
440
556
  def show_modules():
441
- """Show project overview from module-map.json."""
442
- mm = load_index('module-map.json', hint='Run: python git_sync.py --index-only')
557
+ """Show project overview from code-stats.json."""
558
+ mm = load_index('code-stats.json', hint='Run: python git_sync.py --index-only')
443
559
  if mm is None:
444
560
  return
445
561
  print('=== Project Modules ===')
@@ -453,7 +569,7 @@ def show_modules():
453
569
 
454
570
  def show_top_keywords():
455
571
  """Show top keywords by file count."""
456
- ki = load_index('keyword-index.json', hint='Run: python git_sync.py')
572
+ ki = load_index('code-keyword.json', hint='Run: python git_sync.py')
457
573
  if ki is None:
458
574
  return
459
575
  top = sorted(ki.items(), key=lambda x: len(x[1]), reverse=True)[:50]
@@ -464,7 +580,7 @@ def show_top_keywords():
464
580
 
465
581
 
466
582
  def show_components():
467
- meta = load_index('style-meta.json', hint='Run: python build_style_index.py')
583
+ meta = load_index('ui-style-meta.json', hint='Run: python build_style_index.py')
468
584
  if meta is None:
469
585
  return
470
586
  print('=== UI Components Usage ===')
@@ -479,7 +595,7 @@ def show_components():
479
595
 
480
596
 
481
597
  def show_vben():
482
- data = load_index('vben-style-reference.json')
598
+ data = load_index('ref-vben-style.json')
483
599
  if data is None:
484
600
  return
485
601
  print('=== Vben Admin Design Tokens ===')
@@ -538,6 +654,30 @@ def search_wiki_cli(query):
538
654
  print('\n 提示: 读 wiki 文档 (带行号引用) 比读源码更省 token')
539
655
 
540
656
 
657
+
658
+ def search_impact(query):
659
+ """改这个接口/符号影响哪些页面/按钮/模块? (U1: 内存图遍历)"""
660
+ from common.graph_traverse import CodeGraph
661
+ g = CodeGraph(INDEX_DIR)
662
+ result = g.impact(query)
663
+ if result:
664
+ print(result)
665
+ else:
666
+ print('未找到匹配 "{}" 的端点。尝试用部分路径, 如 /veh 或 export'.format(query))
667
+ print('提示: trace-chain 当前覆盖率 31%, 未覆盖的调用不会被列出')
668
+
669
+
670
+ def search_context360(query):
671
+ """符号的360度视图: 端点/函数/处理器/PRD (U1: 内存图遍历)"""
672
+ from common.graph_traverse import CodeGraph
673
+ g = CodeGraph(INDEX_DIR)
674
+ result = g.context360(query)
675
+ if result:
676
+ print(result)
677
+ else:
678
+ print('未找到匹配 "{}" 的符号。'.format(query))
679
+
680
+
541
681
  def print_usage():
542
682
  print('Usage:')
543
683
  print(' search_index.py <keyword> [--platform web|app] - Search code')
@@ -545,6 +685,12 @@ def print_usage():
545
685
  print(' search_index.py --style <type> [--platform p] - UI patterns (table/form/detail/modal/dashboard)')
546
686
  print(' search_index.py --field <name> - Field usage')
547
687
  print(' search_index.py --api <keyword> - API endpoints')
688
+ print(' search_index.py --impact <endpoint> - 改这个接口影响哪些页面/按钮/模块')
689
+ print(' search_index.py --context360 <symbol> - 符号的360度视图(端点/函数/处理器/PRD)')
690
+ print(' search_index.py --coverage - 功能×测试覆盖矩阵(哪些功能没测试)')
691
+ print(' search_index.py --feature <名称> - 功能画像(端点+按钮+测试+页面)')
692
+ print(' search_index.py --workflow <名称> - 业务流程链(盘点有哪几步)')
693
+ print(' search_index.py --routes <关键词> - 前端页面路由(保险->/veh/vehicle/vehAffair/insurance)')
548
694
  print(' search_index.py --components - UI components')
549
695
  print(' search_index.py --wiki <keyword> - Repo Wiki 语义模块文档 (精准)')
550
696
  print(' search_index.py --modules - Project overview')
@@ -554,6 +700,95 @@ def print_usage():
554
700
  print('Platform: web/pc/管理端 -> fywl-ui, app/mobile/h5/移动端 -> Carmg-H5')
555
701
 
556
702
 
703
+ def _search_routes(keyword):
704
+ """查前端页面路由: 优先用 menu-routes-tree.json(getRouters父子树, path准确),
705
+ 回退 menu-routes.json(扁平, component猜的URL可能不准)。
706
+
707
+ 例: _search_routes('保险') -> /veh/vehicle/vehAffair/insurance
708
+ ⚠️ 若结果URL打开404, 说明 tree 缓存过期, 重跑: python .qoder/scripts/extract_routes_tree.py
709
+ """
710
+ import os, json
711
+ kw = keyword.lower()
712
+ # ① 优先: 准确路由树 (path 直接来自后端父子拼接)
713
+ tree_file = os.path.join(INDEX_DIR, 'menu-routes-tree.json')
714
+ if os.path.isfile(tree_file):
715
+ data = json.load(open(tree_file, encoding='utf-8'))
716
+ routes = data.get('routes', [])
717
+ hits = [r for r in routes if kw in (r.get('path','') + r.get('name','') + r.get('component','')).lower()]
718
+ if hits:
719
+ print('=== 页面路由: %s (%d 条, 来源: 准确路由树) ===' % (keyword, len(hits)))
720
+ for r in hits:
721
+ print(' %s -> %s' % (r.get('name', '?'), r.get('path', '')))
722
+ return
723
+ # tree 里没有, 不再回退到不准的旧数据, 直接提示 (避免误导成404)
724
+ print('未找到 "%s" 的路由(已查准确路由树 %d 条)。' % (keyword, len(routes)))
725
+ print('提示: 试试英文关键词, 或重跑刷新: python .qoder/scripts/extract_routes_tree.py')
726
+ return
727
+ # ② 回退: 旧扁平数据 (component猜URL, 可能不准)
728
+ routes_file = os.path.join(INDEX_DIR, 'menu-routes.json')
729
+ if not os.path.isfile(routes_file):
730
+ print('无路由数据。先运行: python .qoder/scripts/extract_routes_tree.py')
731
+ return
732
+ data = json.load(open(routes_file, encoding='utf-8'))
733
+ routes = data.get('routes', [])
734
+ hits = []
735
+ for r in routes:
736
+ searchable = ' '.join([r.get('path',''), r.get('component',''), r.get('name','')]).lower()
737
+ if kw in searchable:
738
+ hits.append(r)
739
+ if not hits:
740
+ print('未找到 "%s" 的路由。' % keyword)
741
+ print('提示: 试试英文关键词, 或刷新路由树: python .qoder/scripts/extract_routes_tree.py')
742
+ return
743
+ print('=== 页面路由: %s (%d 条, 来源: 旧扁平数据, URL可能不准) ===' % (keyword, len(hits)))
744
+ for r in hits:
745
+ print(' %s -> path=%s component=%s' % (r.get('name', '?'), r.get('path', ''), r.get('component', '')))
746
+ print('⚠️ 以上URL可能不准(component猜的)。建议运行 extract_routes_tree.py 用准确路由树。')
747
+
748
+
749
+ def _graph_cli(method, *args):
750
+ """CLI 入口: 调图谱方法(让纯 Qoder IDE/CLI 也能用, 不只 MCP)。
751
+
752
+ CodeGraph 方法(coverage_matrix/feature_overview/impact)走 JSON 索引;
753
+ get_workflow 走 DuckDB(业务操作链)。
754
+ """
755
+ try:
756
+ if method == 'get_workflow':
757
+ # DuckDB 路径
758
+ import kg_duckdb as K
759
+ q = (args[0] if args else '').strip()
760
+ if not q:
761
+ print('请提供模块名, 如: assets / cost / hr / maintenance')
762
+ return
763
+ con = K.get_db()
764
+ rows = con.execute(
765
+ "SELECT seq, state, endpoint, button FROM workflows WHERE module=? ORDER BY seq",
766
+ [q.lower()]
767
+ ).fetchall()
768
+ if not rows:
769
+ mods = [r[0] for r in con.execute("SELECT DISTINCT module FROM workflows ORDER BY module").fetchall()]
770
+ print("未找到模块 '%s'。现有: %s" % (q, ', '.join(mods)))
771
+ return
772
+ print('=== %s 业务操作链 (%d 步) ===' % (q, len(rows)))
773
+ for r in rows:
774
+ print(' %d. [%s] %s — %s' % (r[0], r[1] or '?', r[2] or '', r[3] or ''))
775
+ return
776
+ # JSON 索引路径 (CodeGraph)
777
+ from common.graph_traverse import CodeGraph
778
+ from common.paths import DATA_INDEX_DIR
779
+ g = CodeGraph(str(DATA_INDEX_DIR))
780
+ fn = getattr(g, method, None)
781
+ if not fn:
782
+ print('未知图谱方法: %s' % method)
783
+ return
784
+ result = fn(*args) if args else fn()
785
+ print(result or '(无结果)')
786
+ except ImportError as e:
787
+ print('依赖缺失: %s' % e)
788
+ except Exception as e:
789
+ print('图谱查询失败: %s' % e)
790
+
791
+
557
792
  if __name__ == '__main__':
558
793
  platform = None
559
794
  args = sys.argv[1:]
@@ -575,6 +810,18 @@ if __name__ == '__main__':
575
810
  search_field(args[1])
576
811
  elif args[0] == '--api' and len(args) >= 2:
577
812
  search_api(args[1])
813
+ elif args[0] == '--impact' and len(args) >= 2:
814
+ search_impact(args[1])
815
+ elif args[0] == '--context360' and len(args) >= 2:
816
+ search_context360(args[1])
817
+ elif args[0] == '--coverage':
818
+ _graph_cli('coverage_matrix')
819
+ elif args[0] == '--feature' and len(args) >= 2:
820
+ _graph_cli('feature_overview', args[1])
821
+ elif args[0] == '--workflow' and len(args) >= 2:
822
+ _graph_cli('get_workflow', args[1])
823
+ elif args[0] == '--routes' and len(args) >= 2:
824
+ _search_routes(args[1])
578
825
  elif args[0] == '--wiki' and len(args) >= 2:
579
826
  search_wiki_cli(args[1])
580
827
  elif args[0] == '--components':