@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.
- package/bin/cli.js +261 -11
- package/package.json +1 -1
- package/templates/qoder/agents/insight-planning.md +67 -0
- package/templates/qoder/agents/insight-research.md +61 -0
- package/templates/qoder/agents/prd-reference.md +8 -2
- package/templates/qoder/commands/optional/wl-insight.md +275 -0
- package/templates/qoder/commands/{wl-report.md → optional/wl-report.md} +13 -5
- package/templates/qoder/commands/{wl-spec.md → optional/wl-spec.md} +1 -1
- package/templates/qoder/commands/{wl-status.md → optional/wl-status.md} +28 -2
- package/templates/qoder/commands/wl-code.md +10 -2
- package/templates/qoder/commands/wl-commit.md +1 -1
- package/templates/qoder/commands/wl-design-draw.md +78 -0
- package/templates/qoder/commands/wl-design-scan.md +108 -0
- package/templates/qoder/commands/wl-design-spec.md +154 -0
- package/templates/qoder/commands/wl-design.md +32 -0
- package/templates/qoder/commands/wl-init.md +24 -3
- package/templates/qoder/commands/wl-prd-full.md +226 -0
- package/templates/qoder/commands/wl-prd-quick.md +134 -0
- package/templates/qoder/commands/wl-prd-review.md +104 -0
- package/templates/qoder/commands/wl-prd.md +17 -311
- package/templates/qoder/commands/wl-search.md +66 -30
- package/templates/qoder/commands/wl-task.md +290 -59
- package/templates/qoder/commands/wl-test.md +92 -24
- package/templates/qoder/config.yaml +51 -15
- package/templates/qoder/hooks/inject-workflow-state.py +2 -2
- package/templates/qoder/hooks/session-start.py +82 -51
- package/templates/qoder/rules/wl-pipeline.md +216 -105
- package/templates/qoder/scripts/__pycache__/search_index.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/archive_prd.py +377 -0
- package/templates/qoder/scripts/autotest.py +1715 -0
- package/templates/qoder/scripts/autotest_batch.py +224 -0
- package/templates/qoder/scripts/autotest_run.py +297 -0
- package/templates/qoder/scripts/benchmark.py +210 -209
- package/templates/qoder/scripts/build_style_index.py +444 -4
- package/templates/qoder/scripts/check_carriers.py +238 -0
- package/templates/qoder/scripts/check_mcp.py +298 -0
- package/templates/qoder/scripts/check_qoderwork_consistency.py +166 -0
- package/templates/qoder/scripts/common/events.py +46 -0
- package/templates/qoder/scripts/common/extract.py +419 -0
- package/templates/qoder/scripts/common/graph_traverse.py +533 -0
- package/templates/qoder/scripts/common/paths.py +89 -0
- package/templates/qoder/scripts/common/pip_install.py +144 -0
- package/templates/qoder/scripts/common/platform_guard.py +61 -0
- package/templates/qoder/scripts/common/search_engine.py +205 -205
- package/templates/qoder/scripts/common/terms.py +57 -0
- package/templates/qoder/scripts/common/ts_extract.py +536 -0
- package/templates/qoder/scripts/context_pack.py +73 -13
- package/templates/qoder/scripts/enrich_prompt.py +226 -0
- package/templates/qoder/scripts/eval_prd.py +318 -231
- package/templates/qoder/scripts/export.py +487 -487
- package/templates/qoder/scripts/extract_api_params.py +246 -0
- package/templates/qoder/scripts/extract_routes.py +54 -0
- package/templates/qoder/scripts/extract_routes_tree.py +78 -0
- package/templates/qoder/scripts/fill_prototype.py +707 -0
- package/templates/qoder/scripts/gen_design_doc.py +394 -0
- package/templates/qoder/scripts/git_sync.py +27 -15
- package/templates/qoder/scripts/init_doctor.py +292 -40
- package/templates/qoder/scripts/install_qoderwork.py +366 -9
- package/templates/qoder/scripts/kg.py +708 -0
- package/templates/qoder/scripts/kg_auto_login.py +196 -0
- package/templates/qoder/scripts/kg_build.py +612 -0
- package/templates/qoder/scripts/kg_build_db.py +327 -0
- package/templates/qoder/scripts/kg_duckdb.py +549 -0
- package/templates/qoder/scripts/kg_incremental.py +393 -0
- package/templates/qoder/scripts/kg_link_db.py +224 -0
- package/templates/qoder/scripts/kg_mcp_server.py +801 -0
- package/templates/qoder/scripts/kg_semantic.py +150 -0
- package/templates/qoder/scripts/kg_test_runner.py +241 -0
- package/templates/qoder/scripts/lanhu_stdio_wrapper.py +119 -0
- package/templates/qoder/scripts/learn.py +118 -39
- package/templates/qoder/scripts/learn_aggregate.py +201 -0
- package/templates/qoder/scripts/mcp_launcher.py +359 -0
- package/templates/qoder/scripts/mysql_mcp_server.py +396 -0
- package/templates/qoder/scripts/repo_root.py +106 -0
- package/templates/qoder/scripts/role.py +12 -0
- package/templates/qoder/scripts/run_weekly_update.bat +5 -0
- package/templates/qoder/scripts/run_weekly_update.sh +5 -0
- package/templates/qoder/scripts/search_index.py +307 -60
- package/templates/qoder/scripts/secure-ls.js +5640 -0
- package/templates/qoder/scripts/setup.py +706 -641
- package/templates/qoder/scripts/setup_lanhu.py +963 -0
- package/templates/qoder/scripts/status.py +250 -11
- package/templates/qoder/scripts/sync_carriers.py +259 -0
- package/templates/qoder/scripts/syncgate.py +2 -2
- package/templates/qoder/scripts/task.py +75 -0
- package/templates/qoder/scripts/team_sync.py +60 -4
- package/templates/qoder/scripts/workspace_init.py +1 -1
- package/templates/qoder/skills/design-import/SKILL.md +226 -0
- package/templates/qoder/skills/design-import/figma-workflow.md +81 -0
- package/templates/qoder/skills/design-review/SKILL.md +82 -25
- package/templates/qoder/skills/prd-generator/SKILL.md +184 -60
- package/templates/qoder/skills/prd-review/SKILL.md +18 -1
- package/templates/qoder/skills/prompt-enrich/SKILL.md +90 -0
- package/templates/qoder/skills/prototype-generator/SKILL.md +256 -141
- package/templates/qoder/skills/prototype-generator/SKILL.md.zcode-79180-2af4721f-f9a6-412c-88db-c0af680d211b.tmp +0 -0
- package/templates/qoder/skills/spec-coder/SKILL.md +18 -1
- package/templates/qoder/skills/spec-generator/SKILL.md +18 -1
- package/templates/qoder/skills/test-generator/SKILL.md +15 -2
- package/templates/qoder/skills/wl-code/SKILL.md +55 -36
- package/templates/qoder/skills/wl-commit/SKILL.md +89 -76
- package/templates/qoder/skills/wl-design/SKILL.md +55 -0
- package/templates/qoder/skills/wl-init/SKILL.md +76 -67
- package/templates/qoder/skills/wl-insight/SKILL.md +201 -81
- package/templates/qoder/skills/wl-prd-full/SKILL.md +69 -0
- package/templates/qoder/skills/wl-prd-quick/SKILL.md +49 -0
- package/templates/qoder/skills/wl-prd-review/SKILL.md +34 -0
- package/templates/qoder/skills/wl-report/SKILL.md +131 -107
- package/templates/qoder/skills/wl-search/SKILL.md +141 -75
- package/templates/qoder/skills/wl-spec/SKILL.md +49 -39
- package/templates/qoder/skills/wl-status/SKILL.md +83 -61
- package/templates/qoder/skills/wl-task/SKILL.md +132 -58
- package/templates/qoder/skills/wl-test/SKILL.md +406 -40
- package/templates/qoder/templates/prototype-app.html +13 -8
- package/templates/qoder/templates/prototype-web.html +376 -93
- package/templates/root/AGENTS.md +89 -34
- package/templates/root/requirements.txt +21 -0
- package/templates/root//344/275/277/347/224/250/350/257/264/346/230/216.md +259 -259
- package/templates/root//346/226/260/346/211/213/346/214/207/345/215/227.md +186 -186
- package/templates/qoder/agents/prd-planning.md +0 -57
- package/templates/qoder/agents/prd-research.md +0 -34
- package/templates/qoder/commands/wl-insight.md +0 -51
- package/templates/qoder/scripts/__pycache__/init_doctor.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/__pycache__/setup.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/common/__pycache__/developer.cpython-39.pyc +0 -0
- 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
|