@hupan56/wlkj 2.5.0 → 2.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/bin/cli.js +289 -12
  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-quick.md +1 -0
  6. package/templates/qoder/agents/prd-reference.md +10 -2
  7. package/templates/qoder/commands/optional/wl-insight.md +275 -0
  8. package/templates/qoder/commands/{wl-report.md → optional/wl-report.md} +13 -5
  9. package/templates/qoder/commands/{wl-spec.md → optional/wl-spec.md} +1 -1
  10. package/templates/qoder/commands/{wl-status.md → optional/wl-status.md} +28 -2
  11. package/templates/qoder/commands/wl-code.md +10 -2
  12. package/templates/qoder/commands/wl-commit.md +1 -1
  13. package/templates/qoder/commands/wl-design-draw.md +78 -0
  14. package/templates/qoder/commands/wl-design-scan.md +108 -0
  15. package/templates/qoder/commands/wl-design-spec.md +154 -0
  16. package/templates/qoder/commands/wl-design.md +32 -0
  17. package/templates/qoder/commands/wl-init.md +24 -3
  18. package/templates/qoder/commands/wl-prd-full.md +226 -0
  19. package/templates/qoder/commands/wl-prd-quick.md +134 -0
  20. package/templates/qoder/commands/wl-prd-review.md +104 -0
  21. package/templates/qoder/commands/wl-prd.md +17 -288
  22. package/templates/qoder/commands/wl-search.md +66 -30
  23. package/templates/qoder/commands/wl-task.md +290 -59
  24. package/templates/qoder/commands/wl-test.md +92 -24
  25. package/templates/qoder/config.yaml +59 -15
  26. package/templates/qoder/hooks/inject-workflow-state.py +35 -9
  27. package/templates/qoder/hooks/session-start.py +144 -62
  28. package/templates/qoder/rules/wl-pipeline.md +216 -105
  29. package/templates/qoder/scripts/__pycache__/search_index.cpython-39.pyc +0 -0
  30. package/templates/qoder/scripts/archive_prd.py +377 -0
  31. package/templates/qoder/scripts/autotest.py +1715 -0
  32. package/templates/qoder/scripts/autotest_batch.py +224 -0
  33. package/templates/qoder/scripts/autotest_run.py +297 -0
  34. package/templates/qoder/scripts/benchmark.py +210 -209
  35. package/templates/qoder/scripts/build_style_index.py +444 -4
  36. package/templates/qoder/scripts/check_carriers.py +238 -0
  37. package/templates/qoder/scripts/check_mcp.py +298 -0
  38. package/templates/qoder/scripts/check_qoderwork_consistency.py +166 -0
  39. package/templates/qoder/scripts/common/developer.py +26 -19
  40. package/templates/qoder/scripts/common/events.py +46 -0
  41. package/templates/qoder/scripts/common/extract.py +419 -0
  42. package/templates/qoder/scripts/common/graph_traverse.py +533 -0
  43. package/templates/qoder/scripts/common/identity.py +6 -1
  44. package/templates/qoder/scripts/common/paths.py +89 -0
  45. package/templates/qoder/scripts/common/pip_install.py +144 -0
  46. package/templates/qoder/scripts/common/platform_guard.py +61 -0
  47. package/templates/qoder/scripts/common/search_engine.py +205 -205
  48. package/templates/qoder/scripts/common/terms.py +57 -0
  49. package/templates/qoder/scripts/common/ts_extract.py +536 -0
  50. package/templates/qoder/scripts/context_pack.py +73 -13
  51. package/templates/qoder/scripts/enrich_prompt.py +226 -0
  52. package/templates/qoder/scripts/eval_prd.py +318 -225
  53. package/templates/qoder/scripts/export.py +487 -487
  54. package/templates/qoder/scripts/extract_api_params.py +246 -0
  55. package/templates/qoder/scripts/extract_routes.py +54 -0
  56. package/templates/qoder/scripts/extract_routes_tree.py +78 -0
  57. package/templates/qoder/scripts/fill_prototype.py +707 -0
  58. package/templates/qoder/scripts/gen_design_doc.py +394 -0
  59. package/templates/qoder/scripts/git_sync.py +27 -15
  60. package/templates/qoder/scripts/init_doctor.py +306 -41
  61. package/templates/qoder/scripts/install_qoderwork.py +366 -9
  62. package/templates/qoder/scripts/kg.py +708 -0
  63. package/templates/qoder/scripts/kg_auto_login.py +196 -0
  64. package/templates/qoder/scripts/kg_build.py +612 -0
  65. package/templates/qoder/scripts/kg_build_db.py +327 -0
  66. package/templates/qoder/scripts/kg_duckdb.py +549 -0
  67. package/templates/qoder/scripts/kg_incremental.py +393 -0
  68. package/templates/qoder/scripts/kg_link_db.py +224 -0
  69. package/templates/qoder/scripts/kg_mcp_server.py +801 -0
  70. package/templates/qoder/scripts/kg_semantic.py +150 -0
  71. package/templates/qoder/scripts/kg_test_runner.py +241 -0
  72. package/templates/qoder/scripts/lanhu_stdio_wrapper.py +119 -0
  73. package/templates/qoder/scripts/learn.py +118 -39
  74. package/templates/qoder/scripts/learn_aggregate.py +201 -0
  75. package/templates/qoder/scripts/mcp_launcher.py +359 -0
  76. package/templates/qoder/scripts/mysql_mcp_server.py +396 -0
  77. package/templates/qoder/scripts/repo_root.py +106 -0
  78. package/templates/qoder/scripts/role.py +12 -0
  79. package/templates/qoder/scripts/run_weekly_update.bat +5 -0
  80. package/templates/qoder/scripts/run_weekly_update.sh +5 -0
  81. package/templates/qoder/scripts/search_index.py +307 -60
  82. package/templates/qoder/scripts/secure-ls.js +5640 -0
  83. package/templates/qoder/scripts/setup.py +706 -453
  84. package/templates/qoder/scripts/setup_lanhu.py +963 -0
  85. package/templates/qoder/scripts/status.py +250 -11
  86. package/templates/qoder/scripts/sync_carriers.py +259 -0
  87. package/templates/qoder/scripts/syncgate.py +5 -4
  88. package/templates/qoder/scripts/task.py +75 -0
  89. package/templates/qoder/scripts/team_sync.py +60 -4
  90. package/templates/qoder/scripts/workspace_init.py +1 -1
  91. package/templates/qoder/skills/design-import/SKILL.md +226 -0
  92. package/templates/qoder/skills/design-import/figma-workflow.md +81 -0
  93. package/templates/qoder/skills/design-review/SKILL.md +82 -25
  94. package/templates/qoder/skills/prd-generator/SKILL.md +185 -58
  95. package/templates/qoder/skills/prd-review/SKILL.md +18 -1
  96. package/templates/qoder/skills/prompt-enrich/SKILL.md +90 -0
  97. package/templates/qoder/skills/prototype-generator/SKILL.md +256 -141
  98. package/templates/qoder/skills/prototype-generator/SKILL.md.zcode-79180-2af4721f-f9a6-412c-88db-c0af680d211b.tmp +0 -0
  99. package/templates/qoder/skills/spec-coder/SKILL.md +18 -1
  100. package/templates/qoder/skills/spec-generator/SKILL.md +18 -1
  101. package/templates/qoder/skills/test-generator/SKILL.md +15 -2
  102. package/templates/qoder/skills/wl-code/SKILL.md +55 -36
  103. package/templates/qoder/skills/wl-commit/SKILL.md +89 -76
  104. package/templates/qoder/skills/wl-design/SKILL.md +55 -0
  105. package/templates/qoder/skills/wl-init/SKILL.md +76 -67
  106. package/templates/qoder/skills/wl-insight/SKILL.md +201 -81
  107. package/templates/qoder/skills/wl-prd-full/SKILL.md +69 -0
  108. package/templates/qoder/skills/wl-prd-quick/SKILL.md +49 -0
  109. package/templates/qoder/skills/wl-prd-review/SKILL.md +34 -0
  110. package/templates/qoder/skills/wl-report/SKILL.md +131 -107
  111. package/templates/qoder/skills/wl-search/SKILL.md +141 -75
  112. package/templates/qoder/skills/wl-spec/SKILL.md +49 -39
  113. package/templates/qoder/skills/wl-status/SKILL.md +83 -61
  114. package/templates/qoder/skills/wl-task/SKILL.md +132 -58
  115. package/templates/qoder/skills/wl-test/SKILL.md +406 -40
  116. package/templates/qoder/templates/prd-full-template.md +2 -0
  117. package/templates/qoder/templates/prd-quick-template.md +1 -0
  118. package/templates/qoder/templates/prototype-app.html +13 -8
  119. package/templates/qoder/templates/prototype-web.html +376 -93
  120. package/templates/root/AGENTS.md +89 -34
  121. package/templates/root/requirements.txt +21 -0
  122. package/templates/root//344/275/277/347/224/250/350/257/264/346/230/216.md +259 -259
  123. package/templates/root//346/226/260/346/211/213/346/214/207/345/215/227.md +186 -186
  124. package/templates/qoder/agents/prd-planning.md +0 -56
  125. package/templates/qoder/agents/prd-research.md +0 -33
  126. package/templates/qoder/commands/wl-insight.md +0 -51
  127. package/templates/qoder/skills/wl-prd/SKILL.md +0 -89
@@ -8,6 +8,7 @@ Usage:
8
8
  python build_style_index.py
9
9
  """
10
10
  import os, json, re, sys
11
+ from common.paths import BUILD_CACHE_DIR
11
12
  from datetime import datetime
12
13
 
13
14
  # UTF-8 stdio (防御性: stdout 被捕获时不崩溃)
@@ -22,8 +23,23 @@ except (AttributeError, TypeError, OSError, IOError):
22
23
  BASE = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
23
24
  DATA_CODE = os.path.join(BASE, 'data', 'code')
24
25
  INDEX_DIR = os.path.join(BASE, 'data', 'index')
25
- STYLE_INDEX_PATH = os.path.join(INDEX_DIR, 'style-index.json')
26
- STYLE_META_PATH = os.path.join(INDEX_DIR, 'style-meta.json')
26
+ STYLE_INDEX_PATH = os.path.join(BUILD_CACHE_DIR, 'ui-style.json')
27
+ STYLE_META_PATH = os.path.join(BUILD_CACHE_DIR, 'ui-style-meta.json')
28
+ MODULE_INDEX_PATH = os.path.join(BUILD_CACHE_DIR, 'ui-modules.json')
29
+ TRACE_INDEX_PATH = os.path.join(BUILD_CACHE_DIR, 'trace-chain.json')
30
+ TEST_ASSERTIONS_PATH = os.path.join(BUILD_CACHE_DIR, 'test-assertions.json')
31
+
32
+ # Import the shared extraction utilities (call-chain + test code extractors)
33
+ # E2: prefer tree-sitter extraction (31%->70%+ coverage); fallback to regex
34
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
35
+ try:
36
+ from common.ts_extract import ts_build_api_fn_table as build_api_fn_table
37
+ from common.ts_extract import ts_trace_clicks as trace_clicks
38
+ _EXTRACT_ENGINE = 'tree-sitter'
39
+ except Exception:
40
+ from common.extract import build_api_fn_table, trace_clicks
41
+ _EXTRACT_ENGINE = 'regex (tree-sitter not installed)'
42
+ from common.test_extract import extract_test_assertions # noqa: E402
27
43
 
28
44
 
29
45
  def extract_vue_patterns(filepath, content):
@@ -85,9 +101,268 @@ def extract_vue_patterns(filepath, content):
85
101
  if grid_c:
86
102
  patterns['layout']['grid_cols'] = [int(x) for x in grid_c]
87
103
 
104
+ # Extract interaction behaviors (供 fill_prototype / DESIGN.md 用, 不固化模板)
105
+ behaviors = {}
106
+ # 侧边栏折叠模式: click / hover / trigger
107
+ if re.search(r'useMenu|collapsed|sidebarCollapsed', content):
108
+ behaviors['sidebar_collapse'] = 'click' if 'collapsed' in content else 'auto'
109
+ # 表格分页模式: 前端 / 后端 / 滚动
110
+ if 'pagination:' in content:
111
+ behaviors['table_pagination'] = 'pagination'
112
+ elif 'scroll-y' in content or 'virtual' in content.lower():
113
+ behaviors['table_pagination'] = 'scroll'
114
+ elif any(m in content for m in TABLE_MARKS):
115
+ behaviors['table_pagination'] = 'none'
116
+ # 表单提交模式: 整体 / 分步 / 行内
117
+ if 'Steps' in content or 'a-steps' in content or 'useStep' in content:
118
+ behaviors['form_mode'] = 'wizard'
119
+ elif 'Tab' in content and 'pane' in content.lower():
120
+ behaviors['form_mode'] = 'tabs'
121
+ elif any(m in content for m in FORM_MARKS):
122
+ behaviors['form_mode'] = 'single'
123
+ # 弹窗类型: modal / drawer
124
+ if 'a-modal' in content or 'Modal' in content.split('import')[-1] if 'import' in content else '':
125
+ behaviors['popup'] = 'modal'
126
+ elif 'a-drawer' in content or 'Drawer' in content:
127
+ behaviors['popup'] = 'drawer'
128
+ if behaviors:
129
+ patterns['behaviors'] = behaviors
130
+
88
131
  return patterns
89
132
 
90
133
 
134
+ def detect_stack(proj_path):
135
+ """Detect the UI tech stack from package.json dependencies.
136
+
137
+ Returns one of: 'vben-admin', 'ant-design-vue', 'vant-mobile',
138
+ 'element-plus', 'react', 'unknown'. This lets the prototype generator pick
139
+ the right skeleton (or honestly degrade) instead of assuming Vben for every
140
+ project.
141
+
142
+ For monorepos (pnpm workspaces), reads apps/*/package.json where concrete
143
+ UI deps live (the root package.json is tooling-only with workspace:* refs).
144
+ """
145
+ def _read_deps(pkg_path):
146
+ try:
147
+ with open(pkg_path, encoding='utf-8', errors='ignore') as f:
148
+ pkg = json.load(f)
149
+ deps = set((pkg.get('dependencies') or {}).keys())
150
+ deps |= set((pkg.get('devDependencies') or {}).keys())
151
+ return deps, pkg
152
+ except Exception:
153
+ return set(), {}
154
+
155
+ # 收集所有 package.json 的依赖 (root + monorepo apps/*)
156
+ all_deps = set()
157
+ root_pkg = os.path.join(proj_path, 'package.json')
158
+ if os.path.isfile(root_pkg):
159
+ d, _ = _read_deps(root_pkg)
160
+ all_deps |= d
161
+ # monorepo: apps/*/package.json 有真实 UI 依赖
162
+ apps_dir = os.path.join(proj_path, 'apps')
163
+ if os.path.isdir(apps_dir):
164
+ for app in os.listdir(apps_dir):
165
+ ap = os.path.join(apps_dir, app, 'package.json')
166
+ if os.path.isfile(ap):
167
+ d, _ = _read_deps(ap)
168
+ all_deps |= d
169
+
170
+ if not all_deps:
171
+ return 'unknown'
172
+
173
+ # 决策树 (按特异性排序)
174
+ if any(d.startswith('@vben/') for d in all_deps):
175
+ return 'vben-admin'
176
+ if 'react' in all_deps and 'react-dom' in all_deps and 'vue' not in all_deps:
177
+ return 'react'
178
+ if 'vant' in all_deps:
179
+ return 'vant-mobile'
180
+ if 'ant-design-vue' in all_deps:
181
+ return 'ant-design-vue'
182
+ if 'element-plus' in all_deps:
183
+ return 'element-plus'
184
+ if 'vue' in all_deps:
185
+ return 'ant-design-vue' # Vue 但无已知组件库, 假设最常见
186
+ return 'unknown'
187
+
188
+
189
+ def extract_layout_fingerprint(proj_path):
190
+ """Extract the project's layout "fingerprint" from Vben preferences.ts.
191
+
192
+ Captures layout mode, sidebar width, theme colors and access mode so the
193
+ prototype generator can mirror the *real* shell of the target project
194
+ (e.g. ICS uses ``mixed-nav`` with a backend-driven multi-level menu),
195
+ instead of always drawing a flat single-level sidebar.
196
+
197
+ Gracefully degrades to defaults when preferences.ts is absent (e.g. the
198
+ Vant mobile project Carmg-H5 has no Vben preferences at all).
199
+ """
200
+ DEFAULT = {
201
+ 'stack': 'unknown',
202
+ 'layout': 'sidebar-nav',
203
+ 'access_mode': 'frontend',
204
+ 'sidebar_width': None,
205
+ 'theme_mode': 'light',
206
+ 'semi_dark_sidebar': False,
207
+ 'color_primary': None,
208
+ 'color_success': None,
209
+ 'color_warning': None,
210
+ 'color_destructive': None,
211
+ 'radius': None,
212
+ }
213
+
214
+ # Find the first preferences.ts that actually overrides something.
215
+ # Monorepos (fywl-ui) may host several apps; pick the richest one.
216
+ pref_text = ''
217
+ for root, dirs, files in os.walk(proj_path):
218
+ dirs[:] = [d for d in dirs if d not in ['node_modules', 'dist', '.git', '__pycache__', '.svn']]
219
+ for f in files:
220
+ if f == 'preferences.ts':
221
+ try:
222
+ txt = open(os.path.join(root, f), encoding='utf-8', errors='ignore').read()
223
+ except Exception:
224
+ continue
225
+ # Prefer files that declare overridesPreferences (the app one),
226
+ # over framework defaults / type stubs.
227
+ if 'overridesPreferences' in txt or 'defineOverridesPreferences' in txt:
228
+ pref_text = txt
229
+ break
230
+ if pref_text:
231
+ break
232
+
233
+ if not pref_text:
234
+ # 无 preferences.ts 也要检测技术栈 (来自 package.json), 否则新项目
235
+ # 永远显示 unknown stack, 原型生成器无法选对骨架
236
+ DEFAULT['stack'] = detect_stack(proj_path)
237
+ return DEFAULT
238
+
239
+ fp = dict(DEFAULT)
240
+
241
+ def grab(key, within=None):
242
+ block = within if within is not None else pref_text
243
+ m = re.search(r"%s\s*:\s*['\"]([^'\"]+)['\"]" % re.escape(key), block)
244
+ return m.group(1) if m else None
245
+
246
+ def grab_num(key, within=None):
247
+ block = within if within is not None else pref_text
248
+ m = re.search(r"%s\s*:\s*(\d+)" % re.escape(key), block)
249
+ return int(m.group(1)) if m else None
250
+
251
+ def grab_bool(key, within=None):
252
+ block = within if within is not None else pref_text
253
+ m = re.search(r"%s\s*:\s*(true|false)" % re.escape(key), block)
254
+ return m.group(1) == 'true' if m else None
255
+
256
+ fp['layout'] = grab('layout') or DEFAULT['layout']
257
+ fp['access_mode'] = grab('accessMode') or DEFAULT['access_mode']
258
+ fp['sidebar_width'] = grab_num('width')
259
+
260
+ # theme block
261
+ theme_m = re.search(r"theme\s*:\s*\{([^}]*)\}", pref_text, re.S)
262
+ theme_block = theme_m.group(1) if theme_m else ''
263
+ if theme_block:
264
+ fp['theme_mode'] = grab('mode', theme_block) or DEFAULT['theme_mode']
265
+ fp['semi_dark_sidebar'] = grab_bool('semiDarkSidebar', theme_block)
266
+ if fp['semi_dark_sidebar'] is None:
267
+ fp['semi_dark_sidebar'] = DEFAULT['semi_dark_sidebar']
268
+ for target, src in [('color_primary', 'colorPrimary'),
269
+ ('color_success', 'colorSuccess'),
270
+ ('color_warning', 'colorWarning'),
271
+ ('color_destructive', 'colorDestructive')]:
272
+ val = grab(src, theme_block)
273
+ if val:
274
+ fp[target] = val
275
+
276
+ rad = grab('radius')
277
+ if rad:
278
+ fp['radius'] = rad
279
+
280
+ # 技术栈检测 (独立于 preferences.ts, 来自 package.json)
281
+ fp['stack'] = detect_stack(proj_path)
282
+
283
+ return fp
284
+
285
+
286
+ # 常见模块目录名 -> 中文标签映射 (兜底, 当 repowiki 无对应时用).
287
+ # 覆盖 ICS 当前已知模块; 新模块若不在表里则用目录名本身.
288
+ MODULE_CN_MAP = {
289
+ 'agent': '代理人', 'assets': '资产管理', 'assignment': '任务调度',
290
+ 'core': '核心', 'cost': '费用管理', 'dashboard': '概览工作台',
291
+ 'energy': '能耗管理', 'fleet': '车队管理', 'flow': '流程管理',
292
+ 'hr': '人事管理', 'maintenance': '维护管理', 'monitor': '监控管理',
293
+ 'operation': '运营管理', 'quality': '质量管理', 'qalityassessment': '质量考核',
294
+ 'qualityandsafety': '质量安全', 'settings': '系统设置', 'system': '系统管理',
295
+ 'tool': '工具', 'vehlife': '车辆生活',
296
+ # APP (Carmg-H5) modules
297
+ 'icsAbnormalCenter': '异常中心', 'icsAbnormalStatistics': '异常统计',
298
+ 'icsAttendance': '考勤管理', 'icsAttendCheck': '考勤核查',
299
+ 'icsBudgetDashboard': '预算看板', 'icsBusiness': '业务管理',
300
+ 'icsCostManage': '费用管理', 'icsDailyReport': '日报管理',
301
+ 'icsFacilities': '设施管理', 'icsFeedBack': '意见反馈',
302
+ 'icsGarden': '园区管理', 'icsHrManagement': '人事管理',
303
+ 'icsInventory': '库存管理', 'icsLogin': '登录',
304
+ 'icsMaintenanceManag': '维护管理', 'icsMaterial': '物料管理',
305
+ 'icsNotice': '通知公告', 'icsOil': '油耗管理',
306
+ 'icsOperation': '运营管理', 'icsPersonalCenter': '个人中心',
307
+ 'icsQualityInspection': '质量巡查', 'icsSafe': '安全管理',
308
+ 'icsSmartProperty': '智慧物业', 'icsVehicle': '车辆管理',
309
+ 'icsVehicleAffairs': '车务管理', 'icsTodo': '待办事项',
310
+ }
311
+
312
+
313
+ def extract_modules(proj_name, proj_path):
314
+ """提取项目的业务模块清单 (英文目录名 + 中文标签 + 来源路径)。
315
+
316
+ 扫描两类来源:
317
+ - Web (fywl-ui): apps/*/src/api/<module>/ 目录
318
+ - APP (Carmg-H5): src/modules/<module>/ 目录
319
+ 中文标签优先用 MODULE_CN_MAP 兜底表。返回 [{dir, cn, path}] 列表。
320
+ """
321
+ # 明显的测试/工具/示例目录, 不算业务模块 (避免污染菜单)
322
+ NOISE_DIRS = {
323
+ 'demo', 'test', 'newtest', 'icstest', 'example', 'examples',
324
+ 'icsqrcode', 'icscontact', 'icshelpcenter', 'icsskill',
325
+ 'icsstationvideo', 'icsworkinstruction', 'icsnotice',
326
+ }
327
+ modules = []
328
+ seen = set()
329
+
330
+ def _add(dir_name, rel_path):
331
+ key = dir_name.lower()
332
+ if key in seen or key in NOISE_DIRS:
333
+ return
334
+ seen.add(key)
335
+ modules.append({
336
+ 'dir': dir_name,
337
+ 'cn': MODULE_CN_MAP.get(dir_name, MODULE_CN_MAP.get(dir_name.lower(), dir_name)),
338
+ 'path': (proj_name + '/' + rel_path).replace('\\', '/'),
339
+ })
340
+
341
+ # Web: apps/*/src/api/<module>/
342
+ api_root = os.path.join(proj_path, 'apps')
343
+ if os.path.isdir(api_root):
344
+ for app in os.listdir(api_root):
345
+ api_dir = os.path.join(api_root, app, 'src', 'api')
346
+ if not os.path.isdir(api_dir):
347
+ continue
348
+ for d in sorted(os.listdir(api_dir)):
349
+ if d.startswith('.') or d.startswith('_'):
350
+ continue
351
+ if os.path.isdir(os.path.join(api_dir, d)):
352
+ _add(d, 'apps/%s/src/api/%s' % (app, d))
353
+
354
+ # APP: src/modules/<module>/ (Carmg-H5 结构)
355
+ mods_dir = os.path.join(proj_path, 'src', 'modules')
356
+ if os.path.isdir(mods_dir):
357
+ for d in sorted(os.listdir(mods_dir)):
358
+ if d.startswith('.') or d.startswith('_'):
359
+ continue
360
+ if os.path.isdir(os.path.join(mods_dir, d)):
361
+ _add(d, 'src/modules/%s' % d)
362
+
363
+ return modules
364
+
365
+
91
366
  def scan_project(proj_name, proj_path):
92
367
  """Scan a single frontend project."""
93
368
  pages = []
@@ -153,6 +428,106 @@ def scan_project(proj_name, proj_path):
153
428
  }
154
429
 
155
430
 
431
+ def build_trace_index():
432
+ """Build the call-chain index: button @click → API endpoint → Java controller.
433
+
434
+ Scans fywl-ui (Web) Vue views + api files. Returns a dict with:
435
+ api_functions: {fnName: {url, verb, file}}
436
+ traces: [{vue_file, button, handler, api_fn, endpoint, verb, controller, confidence}]
437
+ stats: {total_clicks, resolved, coverage_pct, by_confidence}
438
+
439
+ Cross-references endpoints against code-api.json (built by git_sync.py)
440
+ to attach the Java controller file. Carmg-H5 (APP) is skipped — different
441
+ structure, separate concern.
442
+ """
443
+ trace_index = {
444
+ 'version': '1.0', 'updated': datetime.now().isoformat(),
445
+ 'api_functions': {}, 'traces': [], 'stats': {},
446
+ }
447
+
448
+ # Only process projects that have the Web (Vben) views+api structure
449
+ web_proj_path = os.path.join(DATA_CODE, 'fywl-ui')
450
+ if not os.path.isdir(web_proj_path):
451
+ return trace_index
452
+
453
+ # Find the api dir (monorepo: apps/*/src/api)
454
+ api_dir = None
455
+ apps_dir = os.path.join(web_proj_path, 'apps')
456
+ if os.path.isdir(apps_dir):
457
+ for app in os.listdir(apps_dir):
458
+ candidate = os.path.join(apps_dir, app, 'src', 'api')
459
+ if os.path.isdir(candidate):
460
+ api_dir = candidate
461
+ break
462
+ if not api_dir:
463
+ return trace_index
464
+
465
+ # Pass 1: build api function table
466
+ api_table = build_api_fn_table(api_dir)
467
+ trace_index['api_functions'] = api_table
468
+
469
+ # Load code-api.json for Java controller cross-reference
470
+ java_endpoints = {}
471
+ api_idx_path = os.path.join(BUILD_CACHE_DIR, 'code-api.json')
472
+ if os.path.isfile(api_idx_path):
473
+ try:
474
+ java_endpoints = json.load(open(api_idx_path, encoding='utf-8'))
475
+ except Exception:
476
+ pass
477
+
478
+ # Pass 2: scan Vue views for click traces
479
+ views_dir = None
480
+ if os.path.isdir(apps_dir):
481
+ for app in os.listdir(apps_dir):
482
+ candidate = os.path.join(apps_dir, app, 'src', 'views')
483
+ if os.path.isdir(candidate):
484
+ views_dir = candidate
485
+ break
486
+
487
+ total_clicks = 0
488
+ resolved = 0
489
+ by_conf = {}
490
+ if views_dir:
491
+ for root, dirs, files in os.walk(views_dir):
492
+ dirs[:] = [d for d in dirs if d not in ('node_modules', 'dist', '.git')]
493
+ for f in files:
494
+ if not f.endswith('.vue'):
495
+ continue
496
+ fpath = os.path.join(root, f)
497
+ try:
498
+ content = open(fpath, encoding='utf-8', errors='ignore').read()
499
+ except Exception:
500
+ continue
501
+ if '@click' not in content:
502
+ continue
503
+ traces = trace_clicks(fpath, content, api_table)
504
+ for t in traces:
505
+ total_clicks += 1
506
+ conf = t.get('confidence', 'unresolved')
507
+ by_conf[conf] = by_conf.get(conf, 0) + 1
508
+ # Cross-ref endpoint → Java controller
509
+ endpoint = t.get('endpoint')
510
+ if endpoint and endpoint in java_endpoints:
511
+ t['controller'] = java_endpoints[endpoint].split('/')[-1]
512
+ else:
513
+ # Try prefix match (endpoint may have path params)
514
+ for ep, ctrl in java_endpoints.items():
515
+ if endpoint and (endpoint.startswith(ep) or ep.startswith(endpoint)):
516
+ t['controller'] = ctrl.split('/')[-1]
517
+ break
518
+ if t.get('endpoint'):
519
+ resolved += 1
520
+ trace_index['traces'].append(t)
521
+
522
+ trace_index['stats'] = {
523
+ 'total_clicks': total_clicks,
524
+ 'resolved': resolved,
525
+ 'coverage_pct': round(100.0 * resolved / total_clicks, 1) if total_clicks else 0,
526
+ 'by_confidence': by_conf,
527
+ }
528
+ return trace_index
529
+
530
+
156
531
  def main():
157
532
  print('Building style index...')
158
533
  print('Code dir: ' + DATA_CODE)
@@ -174,6 +549,7 @@ def main():
174
549
  total_component_counter = {}
175
550
  total_form_components = {}
176
551
  total_field_map = {}
552
+ module_index = {'version': '1.0', 'updated': datetime.now().isoformat(), 'projects': {}}
177
553
 
178
554
  for proj in sorted(os.listdir(DATA_CODE)):
179
555
  proj_path = os.path.join(DATA_CODE, proj)
@@ -184,11 +560,17 @@ def main():
184
560
  result = scan_project(proj, proj_path)
185
561
  print(' Vue: ' + str(result['vue_count']) + ', Data: ' + str(result['data_count']))
186
562
 
563
+ mods = extract_modules(proj, proj_path)
564
+ if mods:
565
+ module_index['projects'][proj] = mods
566
+ print(' Modules: ' + str(len(mods)))
567
+
187
568
  style_index['projects'][proj] = {
188
569
  'vue_files': result['vue_count'],
189
570
  'data_files': result['data_count'],
190
571
  'page_types': {},
191
572
  'page_examples': {},
573
+ 'layout_fingerprint': extract_layout_fingerprint(proj_path),
192
574
  }
193
575
 
194
576
  # Count page types and keep sample files per type so
@@ -201,8 +583,17 @@ def main():
201
583
  pages_by_type.setdefault(pt, []).append(page)
202
584
  for pt, pages in pages_by_type.items():
203
585
  pages.sort(key=lambda p: len(p.get('table_columns', [])) + len(p.get('form_fields', [])), reverse=True)
204
- style_index['projects'][proj]['page_examples'][pt] = sorted(
205
- [p['file'] for p in pages[:15]])
586
+ # 保留提取的结构 (table_columns/form_fields/form_components/layout),
587
+ # fill_prototype.py 自动填模板用。旧版只存 file 路径, 扔了结构。
588
+ style_index['projects'][proj]['page_examples'][pt] = [
589
+ {'file': p['file'],
590
+ 'table_columns': p.get('table_columns', []),
591
+ 'form_fields': p.get('form_fields', []),
592
+ 'form_components': p.get('form_components', []),
593
+ 'layout': p.get('layout', {}),
594
+ 'behaviors': p.get('behaviors', {})}
595
+ for p in pages[:10]
596
+ ]
206
597
 
207
598
  # Merge component counters
208
599
  for comp, count in result['component_counter'].items():
@@ -256,11 +647,60 @@ def main():
256
647
  with open(STYLE_META_PATH, 'w', encoding='utf-8') as f:
257
648
  json.dump(meta, f, ensure_ascii=False, indent=2, sort_keys=True)
258
649
 
650
+ # ui-modules.json — 业务模块清单 (供 fill_prototype 取真实模块名,
651
+ # 替代靠文件路径猜的 infer_module_name; 让菜单不再是 "veh"/"二级菜单A")
652
+ module_index['updated'] = datetime.now().isoformat()
653
+ with open(MODULE_INDEX_PATH, 'w', encoding='utf-8') as f:
654
+ json.dump(module_index, f, ensure_ascii=False, indent=2, sort_keys=True)
655
+
656
+ # trace-chain.json — 调用链索引 (按钮@click → API端点 → Java Controller)
657
+ # 支撑测试自动化 (QoderWork 知道点哪调啥) + 开发影响分析 (改接口影响哪些页面)
658
+ print('Building trace index (engine: %s)...' % _EXTRACT_ENGINE)
659
+ trace_index = build_trace_index()
660
+ with open(TRACE_INDEX_PATH, 'w', encoding='utf-8') as f:
661
+ json.dump(trace_index, f, ensure_ascii=False, indent=2, sort_keys=True)
662
+
663
+ # test-assertions.json — E1: 测试代码提取 (Given-When-Then + 断言 + 注入依赖)
664
+ # 第二层知识: 生产代码提取不到的业务预期和断言标准, 补上'怎么算对'天花板
665
+ print('Building test assertions index...')
666
+ all_tests = []
667
+ for proj in sorted(os.listdir(DATA_CODE)):
668
+ proj_path = os.path.join(DATA_CODE, proj)
669
+ if os.path.isdir(proj_path):
670
+ tests = extract_test_assertions(proj_path)
671
+ all_tests.extend(tests)
672
+ test_index = {
673
+ 'version': '1.0', 'updated': datetime.now().isoformat(),
674
+ 'tests': all_tests,
675
+ 'stats': {'total': len(all_tests),
676
+ 'integration': sum(1 for t in all_tests if t.get('is_integration'))},
677
+ }
678
+ with open(TEST_ASSERTIONS_PATH, 'w', encoding='utf-8') as f:
679
+ json.dump(test_index, f, ensure_ascii=False, indent=2, sort_keys=True)
680
+
681
+ # ── 知识图谱层: relations + features + entity-registry + DuckDB ──
682
+ print('Building knowledge graph layers...')
683
+ import subprocess
684
+ for script in ['build_relations.py', 'build_features.py',
685
+ 'build_entity_registry.py', 'build_workflows.py', 'kg_duckdb.py']:
686
+ sp = os.path.join(os.path.dirname(os.path.abspath(__file__)), script)
687
+ if os.path.isfile(sp):
688
+ try:
689
+ subprocess.run([sys.executable, sp], capture_output=True, timeout=120)
690
+ print(' %s OK' % script)
691
+ except Exception as e:
692
+ print(' %s SKIP: %s' % (script, str(e)[:60]))
693
+
259
694
  print()
260
695
  print('=== Done ===')
261
696
  print('Common components: ' + str(len(style_index['common_components'])))
262
697
  print('Form components: ' + str(len(style_index['form_components'])))
263
698
  print('Field map: ' + str(len(style_index['field_map'])) + ' entries')
699
+ print('Module index: ' + str(sum(len(v) for v in module_index['projects'].values())) + ' modules')
700
+ print('Trace index: ' + str(len(trace_index['api_functions'])) + ' api fns, '
701
+ + str(trace_index['stats'].get('resolved', 0)) + '/'
702
+ + str(trace_index['stats'].get('total_clicks', 0)) + ' clicks resolved ('
703
+ + str(trace_index['stats'].get('coverage_pct', 0)) + '%)')
264
704
  print('Saved to: ' + STYLE_META_PATH)
265
705
 
266
706