@hupan56/wlkj 2.6.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 (125) hide show
  1. package/bin/cli.js +261 -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
@@ -25,6 +25,12 @@ CN_MAP = {
25
25
  '油耗': 'fuel oil', '里程': 'mileage distance', '行驶': 'drive travel',
26
26
  '驾驶员': 'driver', '车队': 'fleet', '违章': 'violation traffic',
27
27
  '事故': 'accident', 'GPS': 'gps location track',
28
+ # 能源/加油/充电卡
29
+ '加油': 'refueling fuel', '加油卡': 'powerCard vehicleCard fuelCard',
30
+ '油卡': 'powerCard vehicleCard fuelCard', '充值': 'recharge topup',
31
+ '加油站': 'gasStation', '能源': 'energy power fuel electricity',
32
+ '充电': 'recharge electricity power', '电卡': 'energyCard powerCard',
33
+ '能耗': 'energy fuel electricity power',
28
34
  # 品质/巡查
29
35
  '品质': 'quality', '巡查': 'inspection patrol', '巡检': 'inspection patrol',
30
36
  '检查': 'check inspect', '隐患': 'hazard risk', '整改': 'rectify fix',
@@ -116,6 +122,12 @@ BUSINESS_PATH_MAP = {
116
122
  '在线率': ['online', 'Online', 'onlineRate', 'OnlineRate'],
117
123
  '告警': ['alarm', 'Alarm', 'alert', 'Alert'],
118
124
  '油耗': ['fuel', 'Fuel', 'oil', 'Oil', 'oilConsumption'],
125
+ '加油': ['refueling', 'Refueling', 'fuel', 'Fuel', 'gasStation', 'topup'],
126
+ '加油卡': ['powerCard', 'PowerCard', 'vehicleCard', 'VehicleCard', 'fuelCard', 'energyCard'],
127
+ '油卡': ['powerCard', 'PowerCard', 'vehicleCard', 'VehicleCard', 'fuelCard'],
128
+ '能源': ['energy', 'Energy', 'refueling', 'electricity', 'powerCard'],
129
+ '充电': ['recharge', 'Recharge', 'electricity', 'powerStation'],
130
+ '能耗': ['energy', 'fuel', 'electricity', 'power'],
119
131
  '里程': ['mileage', 'Mileage', 'distance', 'Distance'],
120
132
  '薪资': ['salary', 'Salary', 'wage', 'Wage', 'pay', 'Pay'],
121
133
  '报表': ['report', 'Report', 'statistics', 'Statistics'],
@@ -211,6 +223,51 @@ def get_cn_map_with_auto():
211
223
  merged.update(auto_expand_from_prds())
212
224
  return merged
213
225
 
226
+
227
+ def expand_chinese_query(query):
228
+ """把中文查询拆成英文搜索词 (单一数据源, search_index + context_pack 共用)。
229
+
230
+ 三级匹配:
231
+ 1. 精确匹配: "异常" 在 CN_MAP → 直接扩展
232
+ 2. 分词匹配: "异常记录" → 拆成 "异常" + "记录", 各自扩展
233
+ (贪心最长匹配: 从长到短扫 CN_MAP 的 key)
234
+ 3. 兜底: 拆不出来的部分, 原样小写 (让英文子串索引做匹配)
235
+
236
+ 同时合并 BUSINESS_PATH_MAP 的路径模式。
237
+
238
+ 例: "营业外合同" 不在 CN_MAP, 但分词能拆出 "合同" → "contract"。
239
+ """
240
+ words = []
241
+ cn_map = get_cn_map_with_auto()
242
+
243
+ # 1. 精确匹配
244
+ if query in cn_map:
245
+ words.extend(cn_map[query].split())
246
+ if query in BUSINESS_PATH_MAP:
247
+ words.extend(p.strip('/-').lower() for p in BUSINESS_PATH_MAP[query])
248
+
249
+ # 2. 分词匹配 (查询比单个 CN_MAP key 长 → 需要拆词)
250
+ if not words and len(query) >= 3:
251
+ # 贪心最长匹配: 把 CN_MAP keys 按长度降序排, 逐个从 query 里找
252
+ remaining = query
253
+ matched_segments = []
254
+ for cn in sorted(cn_map.keys(), key=len, reverse=True):
255
+ if cn in remaining:
256
+ matched_segments.append(cn)
257
+ remaining = remaining.replace(cn, ' ')
258
+ if matched_segments:
259
+ for seg in matched_segments:
260
+ words.extend(cn_map[seg].split())
261
+ if seg in BUSINESS_PATH_MAP:
262
+ words.extend(p.strip('/-').lower() for p in BUSINESS_PATH_MAP[seg])
263
+ # 剩余的英文/数字片段也加进去 (兜底)
264
+ for frag in remaining.split():
265
+ if frag and frag.isascii():
266
+ words.append(frag.lower())
267
+
268
+ return words
269
+
270
+
214
271
  # Stop words filtered out when extracting business terms from PRD text.
215
272
  # (Union of the lists previously duplicated in git_sync.py / parse_prds.py)
216
273
  PRD_STOP_WORDS = {
@@ -0,0 +1,536 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Tree-sitter based extraction for the call-chain index (E2).
4
+
5
+ Replaces the regex-based function-body extraction in common/extract.py with
6
+ AST parsing. Tree-sitter gives us:
7
+ - Exact function boundaries (brace matching is never wrong)
8
+ - All call expressions inside a function (not just requestClient.*)
9
+ - Import resolution (named imports from api modules)
10
+
11
+ This module provides:
12
+ - ts_build_api_fn_table(api_dir) — same output schema as build_api_fn_table,
13
+ but uses AST for precise body boundaries
14
+ - ts_extract_clicks(vue_path, content, api_table) — same schema as trace_clicks,
15
+ but uses AST for function body + call detection
16
+
17
+ Fallback: if tree-sitter is not installed, falls back to the regex version.
18
+ """
19
+ import os
20
+ import re
21
+
22
+ try:
23
+ from tree_sitter import Language, Parser
24
+ import tree_sitter_typescript as tst
25
+ _TS_AVAILABLE = True
26
+ _TS_LANG = Language(tst.language_typescript())
27
+ _PARSER = Parser(_TS_LANG)
28
+ except Exception:
29
+ _TS_AVAILABLE = False
30
+ _PARSER = None
31
+
32
+ from common.extract import (
33
+ build_api_fn_table as regex_build_api_fn_table,
34
+ trace_clicks as regex_trace_clicks,
35
+ parse_api_enum_block, _resolve_url_expr, _find_api_calls_in_text,
36
+ RE_API_IMPORT, RE_CLICK, RE_BTN_TEXT_AFTER_CLICK, RE_COMMON_DOWNLOAD,
37
+ RE_DRAWER_OPEN, RE_MODAL_CONFIRM, RE_ROUTER_PUSH, RE_LOCAL_VUE_IMPORT,
38
+ _resolve_drawer_api as _regex_resolve_drawer,
39
+ )
40
+
41
+
42
+ def _node_text(node, source_bytes):
43
+ """Get text of a tree-sitter node."""
44
+ return source_bytes[node.start_byte:node.end_byte].decode('utf-8', errors='replace')
45
+
46
+
47
+ def _find_calls_in_function_ast(func_node, source_bytes):
48
+ """Find all call_expression nodes inside a function body via AST.
49
+
50
+ Returns list of (callee_text, full_call_text).
51
+ """
52
+ calls = []
53
+ cursor = func_node.walk()
54
+ cursor.goto_first_child()
55
+ # Descend into body
56
+ def visit(node):
57
+ if node.type == 'call_expression':
58
+ callee = node.child_by_field_name('function')
59
+ if callee:
60
+ calls.append((_node_text(callee, source_bytes), _node_text(node, source_bytes)))
61
+ for child in node.children:
62
+ visit(child)
63
+ visit(func_node)
64
+ return calls
65
+
66
+
67
+ def ts_build_api_fn_table(api_dir):
68
+ """Tree-sitter version: build {fnName: {url, verb, file}} from api/*.ts.
69
+
70
+ Uses AST for exact function boundaries + call detection. Falls back to
71
+ regex version if tree-sitter unavailable.
72
+ """
73
+ if not _TS_AVAILABLE:
74
+ return regex_build_api_fn_table(api_dir)
75
+
76
+ table = {}
77
+ if not api_dir or not os.path.isdir(api_dir):
78
+ return table
79
+
80
+ for root, dirs, files in os.walk(api_dir):
81
+ dirs[:] = [d for d in dirs if d not in ('node_modules', 'dist', '__pycache__')]
82
+ for f in files:
83
+ if not f.endswith('.ts'):
84
+ continue
85
+ fpath = os.path.join(root, f)
86
+ try:
87
+ source = open(fpath, 'rb').read()
88
+ except Exception:
89
+ continue
90
+ urls = parse_api_enum_block(source.decode('utf-8', errors='replace'))
91
+
92
+ try:
93
+ tree = _PARSER.parse(source)
94
+ except Exception:
95
+ # Fallback to regex for this file
96
+ continue
97
+
98
+ # Walk AST to find export function / export const = () => {}
99
+ def visit(node):
100
+ if node.type == 'export_statement':
101
+ for child in node.children:
102
+ if child.type == 'function_declaration':
103
+ _process_fn(child, source, urls, fpath, table)
104
+ elif child.type == 'lexical_declaration':
105
+ # export const fnName = (...) => { ... }
106
+ _process_const_fn(child, source, urls, fpath, table)
107
+ elif node.type == 'function_declaration':
108
+ # Non-exported but still processable
109
+ pass
110
+ for child in node.children:
111
+ visit(child)
112
+
113
+ visit(tree.root_node)
114
+
115
+ # Merge with regex results (regex catches some TS-sitter misses)
116
+ regex_table = regex_build_api_fn_table(api_dir)
117
+ for k, v in regex_table.items():
118
+ if k not in table:
119
+ table[k] = v
120
+
121
+ return table
122
+
123
+
124
+ def _process_fn(fn_node, source, urls, fpath, table):
125
+ """Process a function_declaration node."""
126
+ name_node = fn_node.child_by_field_name('name')
127
+ if not name_node:
128
+ return
129
+ fn_name = _node_text(name_node, source)
130
+ body = fn_node.child_by_field_name('body')
131
+ if not body:
132
+ return
133
+ _extract_api_call(fn_name, body, source, urls, fpath, table)
134
+
135
+
136
+ def _process_const_fn(decl_node, source, urls, fpath, table):
137
+ """Process: export const fnName = (params) => { ... } or async."""
138
+ # Find the variable declarator
139
+ for child in decl_node.children:
140
+ if child.type == 'variable_declarator':
141
+ name_node = child.child_by_field_name('name')
142
+ value_node = child.child_by_field_name('value')
143
+ if not name_node or not value_node:
144
+ continue
145
+ fn_name = _node_text(name_node, source)
146
+ # Arrow function body
147
+ body = None
148
+ if value_node.type == 'arrow_function':
149
+ body = value_node.child_by_field_name('body')
150
+ elif value_node.type == 'function':
151
+ body = value_node.child_by_field_name('body')
152
+ if body:
153
+ _extract_api_call(fn_name, body, source, urls, fpath, table)
154
+
155
+
156
+ def _extract_api_call(fn_name, body_node, source, urls, fpath, table):
157
+ """Find requestClient calls inside a function body via AST."""
158
+ calls = _find_calls_in_function_ast(body_node, source)
159
+ entry = None
160
+ for callee, full_call in calls:
161
+ # requestClient.post<...>(Api.key, ...) or requestClient.get(...)
162
+ if callee.startswith('requestClient.'):
163
+ verb = callee.split('.')[-1]
164
+ # Extract first argument (Api.key or string literal)
165
+ m = re.search(r'requestClient\.\w+(?:<[^)]*?>)?\s*\(\s*(Api\.\w+|`[^`]*`|\'[^\']*\'|"[^"]*")', full_call)
166
+ if m:
167
+ url = _resolve_url_expr(m.group(1), urls)
168
+ if url:
169
+ entry = {'url': url, 'verb': verb, 'file': fpath.replace('\\', '/')}
170
+ break
171
+ # commonExport(Api.key, ...)
172
+ elif callee == 'commonExport':
173
+ m = re.search(r'commonExport\s*\(\s*(Api\.\w+)', full_call)
174
+ if m:
175
+ url = _resolve_url_expr(m.group(1), urls)
176
+ if url:
177
+ entry = {'url': url, 'verb': 'post', 'file': fpath.replace('\\', '/')}
178
+ break
179
+ if entry:
180
+ table[fn_name] = entry
181
+
182
+
183
+ def ts_get_fn_body_ast(content, fn_name):
184
+ """Get function body text via tree-sitter AST (exact boundaries).
185
+
186
+ More reliable than _get_fn_body's brace-counting for complex code.
187
+ """
188
+ if not _TS_AVAILABLE:
189
+ from common.extract import _get_fn_body
190
+ return _get_fn_body(content, fn_name)
191
+
192
+ source = content.encode('utf-8')
193
+ try:
194
+ tree = _PARSER.parse(source)
195
+ except Exception:
196
+ from common.extract import _get_fn_body
197
+ return _get_fn_body(content, fn_name)
198
+
199
+ root = tree.root_node
200
+
201
+ def find_fn(node):
202
+ if node.type in ('function_declaration', 'method_definition'):
203
+ name = node.child_by_field_name('name')
204
+ if name and _node_text(name, source) == fn_name:
205
+ body = node.child_by_field_name('body')
206
+ if body:
207
+ return _node_text(body, source)
208
+ for child in node.children:
209
+ result = find_fn(child)
210
+ if result:
211
+ return result
212
+ return None
213
+
214
+ body = find_fn(root)
215
+ if body:
216
+ return body
217
+ # Fallback: arrow function const
218
+ from common.extract import _get_fn_body
219
+ return _get_fn_body(content, fn_name)
220
+
221
+
222
+ def _resolve_props_api_from_parent(parent_content, parent_path, drawer_map, drawer_var, api_table):
223
+ """Resolve props.api() by looking at the PARENT template.
224
+
225
+ When a drawer component's handler calls props.api(), the actual API
226
+ function was passed by the parent via :api="someFn" in the template.
227
+ This finds that binding and checks if someFn is a known api import.
228
+
229
+ Returns the api function name if found, else None.
230
+ """
231
+ # The drawer component tag name in template is the FIRST element of the
232
+ # destructured useVbenDrawer: const [CompTag, drawerVar] = useVbenDrawer(...)
233
+ # Find it: const [<CompTag>, drawerVar] = useVbenDrawer
234
+ comp_tag_m = re.search(
235
+ r"const\s+\[\s*(\w+)\s*,\s*" + re.escape(drawer_var) + r"\s*\]\s*=",
236
+ parent_content)
237
+ if not comp_tag_m:
238
+ return None
239
+ comp_tag = comp_tag_m.group(1)
240
+
241
+ # Find <CompTag ... :api="fnName" ... /> in the template
242
+ # Match :api="xxx" or :api="xxx" within a tag that starts with comp_tag
243
+ tag_pattern = re.compile(
244
+ r'<' + re.escape(comp_tag) + r'\b[^>]*?:api\s*=\s*["\'](\w+)["\']',
245
+ re.S)
246
+ m = tag_pattern.search(parent_content)
247
+ if not m:
248
+ # Also try kebab-case tag name (PascalCase → kebab-case)
249
+ kebab = re.sub(r'([A-Z])', r'-\1', comp_tag).lower().lstrip('-')
250
+ tag_pattern2 = re.compile(
251
+ r'<' + re.escape(kebab) + r'\b[^>]*?:api\s*=\s*["\'](\w+)["\']',
252
+ re.S)
253
+ m = tag_pattern2.search(parent_content)
254
+ if not m:
255
+ return None
256
+
257
+ api_fn_name = m.group(1)
258
+ if api_fn_name in api_table:
259
+ return api_fn_name
260
+
261
+ # Maybe it's a wrapped call like :api="(data) => someFn(data)"
262
+ # Try to extract the function name from arrow expressions
263
+ inner_fn = re.search(r'\b(\w+)\s*\(', api_fn_name)
264
+ if inner_fn and inner_fn.group(1) in api_table:
265
+ return inner_fn.group(1)
266
+
267
+ return None
268
+
269
+
270
+ def _resolve_composable_handler(vue_content, vue_path, handler_name, api_table):
271
+ """Resolve a handler that comes from a composable (useXxx()).
272
+
273
+ Pattern: const { handleAdd } = useLeftBoxUser()
274
+ → find the composable import path → open it → find handleAdd's definition.
275
+ Returns the function body text, or '' if not found.
276
+ """
277
+ # Find: const { ... handlerName ... } = useXxx()
278
+ m = re.search(
279
+ r'const\s*\{([^}]*\b' + re.escape(handler_name) + r'\b[^}]*)\}\s*=\s*use(\w+)\s*\(',
280
+ vue_content)
281
+ if not m:
282
+ return ''
283
+ composable_fn = 'use' + m.group(2) # e.g. useLeftBoxUser
284
+
285
+ # Find import of useXxx from '...'
286
+ imp_m = re.search(
287
+ r'import\s+\{[^}]*\b' + re.escape(composable_fn) + r'\b[^}]*\}\s+from\s+[\'"]([^\'"]+)[\'"]',
288
+ vue_content)
289
+ if not imp_m:
290
+ return ''
291
+ import_path = imp_m.group(1)
292
+
293
+ # Resolve import path to file
294
+ dirname = os.path.dirname(vue_path)
295
+ if import_path.startswith('#/'):
296
+ # Alias: #/ = src/
297
+ src_base = vue_path.replace('\\', '/').split('/src/')[0] + '/src/'
298
+ comp_path = os.path.normpath(os.path.join(src_base, import_path.replace('#/', '')))
299
+ elif import_path.startswith('./') or import_path.startswith('../'):
300
+ comp_path = os.path.normpath(os.path.join(dirname, import_path))
301
+ else:
302
+ return ''
303
+
304
+ # Try .ts extension if not present
305
+ if not os.path.isfile(comp_path):
306
+ for ext in ('.ts', '.vue'):
307
+ if os.path.isfile(comp_path + ext):
308
+ comp_path += ext
309
+ break
310
+
311
+ if not os.path.isfile(comp_path):
312
+ return ''
313
+
314
+ try:
315
+ comp_content = open(comp_path, encoding='utf-8', errors='ignore').read()
316
+ except Exception:
317
+ return ''
318
+
319
+ # Find handler_name's definition in composable (function or const arrow)
320
+ return ts_get_fn_body_ast(comp_content, handler_name) or _regex_fn_body(comp_content, handler_name)
321
+
322
+
323
+ def _regex_fn_body(content, fn_name, max_len=500):
324
+ """Regex fallback for function body (for composable files tree-sitter may miss)."""
325
+ from common.extract import _get_fn_body
326
+ return _get_fn_body(content, fn_name, max_len)
327
+
328
+
329
+ def ts_trace_clicks(vue_path, vue_content, api_table):
330
+ """Tree-sitter enhanced click tracing.
331
+
332
+ Uses AST for function body extraction (more precise than regex).
333
+ The drawer/modal/router logic stays as regex (it's application-level,
334
+ not parse-level). Only the fn_body extraction is upgraded.
335
+ """
336
+ if not _TS_AVAILABLE:
337
+ return regex_trace_clicks(vue_path, vue_content, api_table)
338
+
339
+ dirname = os.path.dirname(vue_path)
340
+
341
+ # 1. Collect api imports (same as regex)
342
+ api_imports = set()
343
+ for im in RE_API_IMPORT.finditer(vue_content):
344
+ for name in im.group(1).split(','):
345
+ name = name.strip().split(' as ')[0].strip()
346
+ if name and name in api_table:
347
+ api_imports.add(name)
348
+
349
+ # 2. Find all @click handlers + button text (same as regex)
350
+ clicks = list(RE_CLICK.finditer(vue_content))
351
+ if not clicks:
352
+ return []
353
+
354
+ # 3. Build drawer map (E2 fix: also resolve #/ and @/ alias imports,
355
+ # not just local ./xxx.vue — many ICS drawers are imported via
356
+ # #/views/... alias paths which the original RE_LOCAL_VUE_IMPORT missed)
357
+ drawer_map = {}
358
+ # Collect ALL vue imports (local + alias + defineAsyncComponent) into a name→path map
359
+ all_vue_imports = {} # {import_name: import_path}
360
+ for im in RE_LOCAL_VUE_IMPORT.finditer(vue_content):
361
+ all_vue_imports[im.group(1)] = im.group(2)
362
+ # Also match alias imports: import xxx from '#/views/....vue' or '@/views/....vue'
363
+ for im in re.finditer(
364
+ r"import\s+(\w+)\s+from\s+['\"](?:#|@)/views/([^'\"]+\.vue)['\"]",
365
+ vue_content):
366
+ all_vue_imports[im.group(1)] = '#/views/' + im.group(2)
367
+ # Also match: const xxx = defineAsyncComponent(() => import('./xxx.vue'))
368
+ # — ICS uses this pattern extensively for drawer components
369
+ for im in re.finditer(
370
+ r"(?:const|let)\s+(\w+)\s*=\s*defineAsyncComponent\s*\(\s*\(\)\s*=>\s*import\s*\(\s*['\"]([^'\"]+\.vue)['\"]\s*\)\s*\)",
371
+ vue_content):
372
+ all_vue_imports[im.group(1)] = im.group(2)
373
+
374
+ for dm in re.finditer(
375
+ r"const\s+\[\s*\w+\s*,\s*(\w+)\s*\]\s*=\s*useVben(?:Drawer|Modal)\s*\(\s*\{([^}]*)\}",
376
+ vue_content, re.S):
377
+ drawer_var = dm.group(1)
378
+ decl_body = dm.group(2)
379
+ cm = re.search(r"connectedComponent:\s*(\w+)", decl_body)
380
+ if not cm:
381
+ continue
382
+ comp_import_name = cm.group(1)
383
+ if comp_import_name in all_vue_imports:
384
+ drawer_map[drawer_var] = all_vue_imports[comp_import_name]
385
+
386
+ traces = []
387
+
388
+ for cm in clicks:
389
+ handler_expr = cm.group(1).strip()
390
+ btn_text = ''
391
+ tm = RE_BTN_TEXT_AFTER_CLICK.search(vue_content, cm.start())
392
+ if tm:
393
+ btn_text = re.sub(r'\{\{[^}]*\}\}', '', tm.group(1).strip())[:12]
394
+
395
+ if handler_expr.startswith('$') or '.splice(' in handler_expr or '.push(' in handler_expr:
396
+ continue
397
+
398
+ handler_name = handler_expr.split('(')[0].split('.')[0].strip()
399
+ if not handler_name or handler_name in ('undefined', '', 'true', 'false'):
400
+ continue
401
+ if handler_name in ('item', 'emit', 'row', 'data') and '.' not in handler_expr:
402
+ continue
403
+
404
+ # ★ Use tree-sitter for function body (the E2 upgrade)
405
+ fn_body = ts_get_fn_body_ast(vue_content, handler_name)
406
+
407
+ trace = {
408
+ 'vue_file': vue_path.replace('\\', '/'),
409
+ 'button': btn_text,
410
+ 'handler': handler_name,
411
+ 'api_fn': None, 'endpoint': None, 'verb': None,
412
+ 'confidence': None,
413
+ }
414
+
415
+ # Pattern B: direct api call
416
+ hits = _find_api_calls_in_text(fn_body, api_table, api_imports)
417
+ if hits:
418
+ fn, url, verb = hits[0]
419
+ trace.update(api_fn=fn, endpoint=url, verb=verb, confidence='high')
420
+ traces.append(trace)
421
+ continue
422
+
423
+ # Pattern D: drawer open
424
+ open_m = RE_DRAWER_OPEN.search(fn_body)
425
+ if open_m:
426
+ drawer_var = open_m.group(1)
427
+ comp_rel = drawer_map.get(drawer_var)
428
+ if comp_rel:
429
+ # Resolve component path: local ./xxx.vue, alias #/views/..., or #/components/...
430
+ vue_path_norm = vue_path.replace('\\', '/') # normalize for split
431
+ if comp_rel.startswith('#/views/'):
432
+ alias_path = comp_rel.replace('#/views/', '')
433
+ src_base = vue_path_norm.split('/src/views/')[0] + '/src/views/'
434
+ comp_path = os.path.normpath(os.path.join(src_base, alias_path))
435
+ elif comp_rel.startswith('#/components/') or comp_rel.startswith('@/components/'):
436
+ alias_path = comp_rel.replace('#/components/', '').replace('@/components/', '')
437
+ src_base = vue_path_norm.split('/src/')[0] + '/src/components/'
438
+ comp_path = os.path.normpath(os.path.join(src_base, alias_path))
439
+ else:
440
+ comp_path = os.path.normpath(os.path.join(dirname, comp_rel))
441
+ if os.path.isfile(comp_path):
442
+ try:
443
+ comp_content = open(comp_path, encoding='utf-8', errors='ignore').read()
444
+ comp_api_imports = set()
445
+ for im in RE_API_IMPORT.finditer(comp_content):
446
+ for name in im.group(1).split(','):
447
+ name = name.strip().split(' as ')[0].strip()
448
+ if name in api_table:
449
+ comp_api_imports.add(name)
450
+ resolved = False
451
+ # First try: find a confirm handler that directly calls api
452
+ for confirm_name in ('handleConfirm', 'onConfirm', 'handleSubmit', 'onOk'):
453
+ cbody = ts_get_fn_body_ast(comp_content, confirm_name)
454
+ if cbody:
455
+ chits = _find_api_calls_in_text(cbody, api_table, comp_api_imports)
456
+ if chits:
457
+ fn, url, verb = chits[0]
458
+ trace.update(api_fn=fn, endpoint=url, verb=verb, confidence='medium')
459
+ resolved = True
460
+ break
461
+
462
+ # Second try: if component uses props.api ANYWHERE (not just
463
+ # in confirm handler — could be in grid query, watch, etc.),
464
+ # resolve via parent template :api="xxx" binding.
465
+ # This is the key insight: don't search for WHICH handler calls
466
+ # props.api — just check IF props.api exists, then find the API
467
+ # from the parent. Humans do exactly this.
468
+ if not resolved and 'props.' in comp_content and re.search(
469
+ r'props\.\w+\s*\(', comp_content):
470
+ parent_api_fn = _resolve_props_api_from_parent(
471
+ vue_content, vue_path, drawer_map, drawer_var, api_table)
472
+ if parent_api_fn:
473
+ info = api_table[parent_api_fn]
474
+ trace.update(api_fn=parent_api_fn, endpoint=info['url'],
475
+ verb=info.get('verb', '?'), confidence='medium')
476
+ resolved = True
477
+
478
+ # Third try: component imports API functions directly (no
479
+ # props.api) — e.g. a selector drawer that calls getList in
480
+ # its grid query. Take the first imported api fn as the
481
+ # associated endpoint (lower confidence — we know the drawer
482
+ # USES this API but can't pinpoint which button triggers it).
483
+ if not resolved and comp_api_imports:
484
+ first_fn = sorted(comp_api_imports)[0]
485
+ info = api_table[first_fn]
486
+ trace.update(api_fn=first_fn, endpoint=info['url'],
487
+ verb=info.get('verb', '?'), confidence='low')
488
+ resolved = True
489
+ except Exception:
490
+ pass
491
+ if trace['endpoint']:
492
+ traces.append(trace)
493
+ continue
494
+
495
+ # Modal.confirm
496
+ mc_m = RE_MODAL_CONFIRM.search(fn_body)
497
+ if mc_m:
498
+ onok_body = fn_body[mc_m.end():]
499
+ hits = _find_api_calls_in_text(onok_body[:400], api_table, api_imports)
500
+ if hits:
501
+ fn, url, verb = hits[0]
502
+ trace.update(api_fn=fn, endpoint=url, verb=verb, confidence='low')
503
+ traces.append(trace)
504
+ continue
505
+
506
+ # router.push
507
+ if fn_body and RE_ROUTER_PUSH.search(fn_body):
508
+ trace['confidence'] = 'deferred'
509
+ traces.append(trace)
510
+ continue
511
+
512
+ # Pattern E: imported function used directly as @click handler
513
+ # (e.g. @click="toContract" where toContract is an imported api fn)
514
+ if not fn_body and handler_name in api_table:
515
+ info = api_table[handler_name]
516
+ trace.update(api_fn=handler_name, endpoint=info['url'],
517
+ verb=info.get('verb', '?'), confidence='high')
518
+ traces.append(trace)
519
+ continue
520
+
521
+ # Pattern F: composable-returned method
522
+ # (e.g. const { handleAdd } = useLeftBoxUser())
523
+ if not fn_body:
524
+ comp_body = _resolve_composable_handler(vue_content, vue_path, handler_name, api_table)
525
+ if comp_body:
526
+ hits = _find_api_calls_in_text(comp_body, api_table, set(api_table.keys()))
527
+ if hits:
528
+ fn, url, verb = hits[0]
529
+ trace.update(api_fn=fn, endpoint=url, verb=verb, confidence='medium')
530
+ traces.append(trace)
531
+ continue
532
+
533
+ trace['confidence'] = 'unresolved'
534
+ traces.append(trace)
535
+
536
+ return traces