@hupan56/wlkj 3.0.0 → 3.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (237) hide show
  1. package/bin/cli.js +1435 -1373
  2. package/package.json +1 -1
  3. package/templates/qoder/commands/optional/wl-insight.md +6 -4
  4. package/templates/qoder/commands/optional/wl-report.md +20 -39
  5. package/templates/qoder/commands/optional/wl-status.md +4 -4
  6. package/templates/qoder/commands/wl-code.md +2 -2
  7. package/templates/qoder/commands/wl-design.md +5 -5
  8. package/templates/qoder/commands/wl-init.md +3 -3
  9. package/templates/qoder/commands/wl-prd.md +26 -13
  10. package/templates/qoder/commands/wl-req.md +3 -3
  11. package/templates/qoder/commands/wl-search.md +78 -47
  12. package/templates/qoder/commands/wl-task.md +343 -40
  13. package/templates/qoder/commands/wl-test.md +10 -10
  14. package/templates/qoder/config.yaml +35 -13
  15. package/templates/qoder/hooks/post-tool-use.py +181 -0
  16. package/templates/qoder/hooks/session-start.py +82 -1
  17. package/templates/qoder/hooks/stop-eval.py +207 -0
  18. package/templates/qoder/hooks/user-prompt-submit.py +139 -0
  19. package/templates/qoder/rules/wl-pipeline.md +70 -8
  20. package/templates/qoder/scripts/README.md +78 -115
  21. package/templates/qoder/scripts/capability/__init__.py +26 -0
  22. package/templates/qoder/scripts/capability/__main__.py +72 -0
  23. package/templates/qoder/scripts/capability/adapters/__init__.py +29 -0
  24. package/templates/qoder/scripts/capability/adapters/cli.py +316 -0
  25. package/templates/qoder/scripts/capability/adapters/mcp.py +334 -0
  26. package/templates/qoder/scripts/capability/adapters/qw.py +271 -0
  27. package/templates/qoder/scripts/capability/caps/__init__.py +27 -0
  28. package/templates/qoder/scripts/capability/caps/context.py +36 -0
  29. package/templates/qoder/scripts/capability/caps/cron.py +50 -0
  30. package/templates/qoder/scripts/capability/caps/identity.py +43 -0
  31. package/templates/qoder/scripts/capability/caps/memory.py +71 -0
  32. package/templates/qoder/scripts/capability/caps/notify.py +41 -0
  33. package/templates/qoder/scripts/capability/caps/present.py +48 -0
  34. package/templates/qoder/scripts/capability/caps/repo.py +31 -0
  35. package/templates/qoder/scripts/capability/caps/sandbox.py +43 -0
  36. package/templates/qoder/scripts/capability/chain.py +92 -0
  37. package/templates/qoder/scripts/capability/memory_chain.py +41 -0
  38. package/templates/qoder/scripts/capability/registry.py +246 -0
  39. package/templates/qoder/scripts/capability/registry_mcp.py +250 -0
  40. package/templates/qoder/scripts/capability/smoke_test.py +211 -0
  41. package/templates/qoder/scripts/capability/smoke_test_report.json +94 -0
  42. package/templates/qoder/scripts/{report → deployment/setup}/__init__.py +11 -11
  43. package/templates/qoder/scripts/{setup → deployment/setup}/carriers.py +33 -26
  44. package/templates/qoder/scripts/deployment/setup/carriers_verify.py +148 -0
  45. package/templates/qoder/scripts/{setup → deployment/setup}/init_doctor.py +40 -16
  46. package/templates/qoder/scripts/{setup → deployment/setup}/install_qoderwork.py +20 -12
  47. package/templates/qoder/scripts/{setup → deployment/setup}/platform_doctor.py +8 -2
  48. package/templates/qoder/scripts/{setup → deployment/setup}/repo_root.py +113 -112
  49. package/templates/qoder/scripts/{setup → deployment/setup}/setup.py +31 -11
  50. package/templates/qoder/scripts/{setup → deployment/setup}/setup_lanhu.py +13 -7
  51. package/templates/qoder/scripts/domain/__init__.py +0 -0
  52. package/templates/qoder/scripts/domain/design/__init__.py +0 -0
  53. package/templates/qoder/scripts/{report → domain/design}/fill_prototype.py +12 -6
  54. package/templates/qoder/scripts/{report → domain/design}/gen_design_doc.py +8 -2
  55. package/templates/qoder/scripts/{setup → domain/kg}/__init__.py +11 -11
  56. package/templates/qoder/scripts/domain/kg/build/__init__.py +0 -0
  57. package/templates/qoder/scripts/{kg → domain/kg/build}/build_entity_registry.py +7 -2
  58. package/templates/qoder/scripts/{kg → domain/kg/build}/build_relations.py +7 -2
  59. package/templates/qoder/scripts/{kg → domain/kg/build}/build_style_index.py +13 -7
  60. package/templates/qoder/scripts/{kg → domain/kg/build}/build_workflows.py +7 -2
  61. package/templates/qoder/scripts/{kg → domain/kg/build}/kg_build.py +76 -9
  62. package/templates/qoder/scripts/{kg → domain/kg/build}/kg_build_db.py +7 -2
  63. package/templates/qoder/scripts/{kg → domain/kg/build}/kg_incremental.py +11 -6
  64. package/templates/qoder/scripts/{report → domain/kg/build}/learn_aggregate.py +9 -4
  65. package/templates/qoder/scripts/domain/kg/extract/__init__.py +0 -0
  66. package/templates/qoder/scripts/{common → domain/kg/extract}/extract.py +430 -419
  67. package/templates/qoder/scripts/{common → domain/kg/extract}/test_extract.py +115 -115
  68. package/templates/qoder/scripts/{common → domain/kg/extract}/ts_extract.py +614 -536
  69. package/templates/qoder/scripts/domain/kg/graph/__init__.py +0 -0
  70. package/templates/qoder/scripts/{kg → domain/kg/graph}/kg_link_db.py +7 -2
  71. package/templates/qoder/scripts/domain/kg/graph/kg_semantic.py +683 -0
  72. package/templates/qoder/scripts/{kg → domain/kg}/kg.py +263 -30
  73. package/templates/qoder/scripts/{common → domain/kg}/kg_capabilities.py +182 -182
  74. package/templates/qoder/scripts/domain/kg/search/__init__.py +0 -0
  75. package/templates/qoder/scripts/{kg → domain/kg/search}/context_pack.py +30 -9
  76. package/templates/qoder/scripts/{kg → domain/kg/search}/enrich_prompt.py +10 -4
  77. package/templates/qoder/scripts/{kg → domain/kg/search}/prefetch.py +33 -8
  78. package/templates/qoder/scripts/{common → domain/kg/search}/search_engine.py +207 -205
  79. package/templates/qoder/scripts/{kg → domain/kg/search}/search_index.py +143 -77
  80. package/templates/qoder/scripts/domain/kg/server/__init__.py +0 -0
  81. package/templates/qoder/scripts/domain/kg/server/kgd.py +549 -0
  82. package/templates/qoder/scripts/domain/kg/server/perf_bench.py +197 -0
  83. package/templates/qoder/scripts/domain/kg/storage/__init__.py +0 -0
  84. package/templates/qoder/scripts/{kg → domain/kg/storage}/kg_duckdb.py +38 -14
  85. package/templates/qoder/scripts/domain/learning/__init__.py +0 -0
  86. package/templates/qoder/scripts/{report → domain/learning}/learn.py +8 -3
  87. package/templates/qoder/scripts/{mcp → domain/report}/__init__.py +11 -11
  88. package/templates/qoder/scripts/{report → domain/report}/add_session.py +9 -3
  89. package/templates/qoder/scripts/{report → domain/report}/export.py +10 -5
  90. package/templates/qoder/scripts/{report → domain/report}/report.py +9 -4
  91. package/templates/qoder/scripts/domain/report/report_snapshot.py +261 -0
  92. package/templates/qoder/scripts/{report → domain/report}/role.py +10 -4
  93. package/templates/qoder/scripts/{report → domain/report}/status.py +112 -9
  94. package/templates/qoder/scripts/domain/report/status_snapshot.py +191 -0
  95. package/templates/qoder/scripts/domain/requirement/__init__.py +0 -0
  96. package/templates/qoder/scripts/{report → domain/requirement}/archive_prd.py +8 -2
  97. package/templates/qoder/scripts/{report → domain/requirement}/req.py +228 -222
  98. package/templates/qoder/scripts/{kg → domain/task}/__init__.py +11 -11
  99. package/templates/qoder/scripts/{task → domain/task}/git_sync.py +40 -21
  100. package/templates/qoder/scripts/{task → domain/task}/syncgate.py +13 -8
  101. package/templates/qoder/scripts/{task → domain/task}/task.py +13 -5
  102. package/templates/qoder/scripts/{task → domain/task}/task_lifecycle.py +606 -596
  103. package/templates/qoder/scripts/{task → domain/task}/task_query.py +162 -161
  104. package/templates/qoder/scripts/{task → domain/task}/task_relations.py +425 -424
  105. package/templates/qoder/scripts/{task → domain/task}/team_sync.py +9 -4
  106. package/templates/qoder/scripts/domain/task/zentao_sync.py +370 -0
  107. package/templates/qoder/scripts/foundation/__init__.py +0 -0
  108. package/templates/qoder/scripts/{common → foundation}/bootstrap.py +10 -10
  109. package/templates/qoder/scripts/foundation/core/__init__.py +0 -0
  110. package/templates/qoder/scripts/{common → foundation/core}/cmd_registry.py +1 -1
  111. package/templates/qoder/scripts/{common → foundation/core}/config.py +1 -1
  112. package/templates/qoder/scripts/{common → foundation/core}/paths.py +706 -586
  113. package/templates/qoder/scripts/foundation/data/__init__.py +0 -0
  114. package/templates/qoder/scripts/{common → foundation/data}/contract.py +2 -2
  115. package/templates/qoder/scripts/{common → foundation/data}/events.py +2 -2
  116. package/templates/qoder/scripts/{common → foundation/data}/result.py +1 -1
  117. package/templates/qoder/scripts/{common → foundation/data}/task_utils.py +427 -404
  118. package/templates/qoder/scripts/foundation/identity/__init__.py +0 -0
  119. package/templates/qoder/scripts/{common → foundation/identity}/check_publish.py +103 -98
  120. package/templates/qoder/scripts/{common → foundation/identity}/developer.py +239 -239
  121. package/templates/qoder/scripts/{common → foundation/identity}/guard.py +2 -2
  122. package/templates/qoder/scripts/{common → foundation/identity}/identity.py +14 -10
  123. package/templates/qoder/scripts/{common → foundation/identity}/roles.py +1 -1
  124. package/templates/qoder/scripts/foundation/integrations/__init__.py +0 -0
  125. package/templates/qoder/scripts/{common → foundation/integrations}/active_task.py +30 -15
  126. package/templates/qoder/scripts/{common → foundation/integrations}/feishu.py +2 -2
  127. package/templates/qoder/scripts/{common → foundation/integrations}/terms.py +3 -3
  128. package/templates/qoder/scripts/foundation/integrations/zentao_client.py +220 -0
  129. package/templates/qoder/scripts/foundation/io/__init__.py +0 -0
  130. package/templates/qoder/scripts/{common → foundation/io}/filelock.py +1 -1
  131. package/templates/qoder/scripts/{common → foundation/io}/reqid.py +1 -1
  132. package/templates/qoder/scripts/foundation/protocol/__init__.py +0 -0
  133. package/templates/qoder/scripts/foundation/protocol/duckdb_conn.py +39 -0
  134. package/templates/qoder/scripts/{common → foundation/protocol}/mcp_base.py +1 -1
  135. package/templates/qoder/scripts/foundation/utils/__init__.py +0 -0
  136. package/templates/qoder/scripts/{common → foundation/utils}/eval_api.py +12 -1
  137. package/templates/qoder/scripts/{common → foundation/utils}/pip_install.py +23 -6
  138. package/templates/qoder/scripts/{common → foundation/utils}/platform_guard.py +1 -1
  139. package/templates/qoder/scripts/orchestration/__init__.py +0 -0
  140. package/templates/qoder/scripts/orchestration/wlkj.py +185 -0
  141. package/templates/qoder/scripts/protocol/__init__.py +0 -0
  142. package/templates/qoder/scripts/protocol/browser/README.md +207 -0
  143. package/templates/qoder/scripts/protocol/browser/SKILL.md +265 -0
  144. package/templates/qoder/scripts/protocol/browser/config.env +9 -0
  145. package/templates/qoder/scripts/protocol/browser/references/cdp-api.md +110 -0
  146. package/templates/qoder/scripts/protocol/browser/references/migration-2.5.3.md +72 -0
  147. package/templates/qoder/scripts/protocol/browser/references/site-patterns/.gitkeep +0 -0
  148. package/templates/qoder/scripts/protocol/browser/scripts/browser-discovery.mjs +138 -0
  149. package/templates/qoder/scripts/protocol/browser/scripts/cdp-proxy.mjs +672 -0
  150. package/templates/qoder/scripts/protocol/browser/scripts/check-deps.mjs +206 -0
  151. package/templates/qoder/scripts/protocol/browser/scripts/find-url.mjs +253 -0
  152. package/templates/qoder/scripts/protocol/browser/scripts/match-site.mjs +46 -0
  153. package/templates/qoder/scripts/protocol/browser/templates/config.env.template +9 -0
  154. package/templates/qoder/scripts/protocol/mcp/__init__.py +11 -0
  155. package/templates/qoder/scripts/protocol/mcp/kg_mcp_server.py +478 -0
  156. package/templates/qoder/scripts/{mcp → protocol/mcp}/lanhu_stdio_wrapper.py +11 -5
  157. package/templates/qoder/scripts/{mcp → protocol/mcp}/mcp_doctor.py +47 -14
  158. package/templates/qoder/scripts/protocol/mcp/mcp_launcher.py +600 -0
  159. package/templates/qoder/scripts/{mcp → protocol/mcp}/mysql_mcp_server.py +181 -67
  160. package/templates/qoder/scripts/protocol/mcp/zentao_mcp_server.py +1920 -0
  161. package/templates/qoder/scripts/protocol/transports/__init__.py +56 -0
  162. package/templates/qoder/scripts/protocol/transports/base.py +87 -0
  163. package/templates/qoder/scripts/protocol/transports/cli.py +132 -0
  164. package/templates/qoder/scripts/protocol/transports/http.py +141 -0
  165. package/templates/qoder/scripts/protocol/transports/stdio.py +293 -0
  166. package/templates/qoder/scripts/run_weekly_update.bat +27 -18
  167. package/templates/qoder/scripts/run_weekly_update.sh +22 -13
  168. package/templates/qoder/scripts/validation/__init__.py +0 -0
  169. package/templates/qoder/scripts/validation/eval/__init__.py +0 -0
  170. package/templates/qoder/scripts/validation/eval/perf-report-2026-06-24.md +139 -0
  171. package/templates/qoder/scripts/validation/eval/qwork_harness.py +617 -0
  172. package/templates/qoder/scripts/validation/eval/report-commands.md +220 -0
  173. package/templates/qoder/scripts/validation/eval/transcript_timing.py +386 -0
  174. package/templates/qoder/scripts/validation/metrics/__init__.py +0 -0
  175. package/templates/qoder/scripts/{report → validation/metrics}/eval_prd.py +17 -10
  176. package/templates/qoder/scripts/validation/metrics/usability_score.py +750 -0
  177. package/templates/qoder/scripts/validation/test/__init__.py +11 -0
  178. package/templates/qoder/scripts/validation/test/assertion_gen.py +551 -0
  179. package/templates/qoder/scripts/{test → validation/test}/autotest.py +64 -4
  180. package/templates/qoder/scripts/{common → validation/test}/autotest_auth.py +1 -1
  181. package/templates/qoder/scripts/{test → validation/test}/autotest_batch.py +16 -3
  182. package/templates/qoder/scripts/{test → validation/test}/autotest_data.py +17 -12
  183. package/templates/qoder/scripts/validation/test/autotest_opencli.py +646 -0
  184. package/templates/qoder/scripts/{test → validation/test}/autotest_run.py +17 -3
  185. package/templates/qoder/scripts/validation/test/autotest_webaccess.py +571 -0
  186. package/templates/qoder/scripts/{test → validation/test}/benchmark.py +12 -6
  187. package/templates/qoder/scripts/{test → validation/test}/kg_auto_login.py +8 -2
  188. package/templates/qoder/scripts/{test → validation/test}/kg_test_runner.py +7 -2
  189. package/templates/qoder/scripts/{test → validation/test}/page_probe.py +8 -3
  190. package/templates/qoder/scripts/validation/test/smoke_all_commands.py +170 -0
  191. package/templates/qoder/settings.json +30 -0
  192. package/templates/qoder/skills/design-import/SKILL.md +22 -13
  193. package/templates/qoder/skills/design-review/SKILL.md +19 -10
  194. package/templates/qoder/skills/prd-generator/SKILL.md +22 -18
  195. package/templates/qoder/skills/prd-review/SKILL.md +6 -4
  196. package/templates/qoder/skills/prototype-generator/SKILL.md +63 -14
  197. package/templates/qoder/skills/spec-coder/SKILL.md +22 -13
  198. package/templates/qoder/skills/spec-generator/SKILL.md +22 -10
  199. package/templates/qoder/skills/test-generator/SKILL.md +7 -5
  200. package/templates/qoder/skills/wl-code/SKILL.md +23 -11
  201. package/templates/qoder/skills/wl-commit/SKILL.md +9 -8
  202. package/templates/qoder/skills/wl-design/SKILL.md +5 -3
  203. package/templates/qoder/skills/wl-init/SKILL.md +6 -6
  204. package/templates/qoder/skills/wl-insight/SKILL.md +23 -10
  205. package/templates/qoder/skills/wl-prd-full/SKILL.md +57 -6
  206. package/templates/qoder/skills/wl-prd-quick/SKILL.md +3 -3
  207. package/templates/qoder/skills/wl-prd-review/SKILL.md +16 -4
  208. package/templates/qoder/skills/wl-report/SKILL.md +25 -23
  209. package/templates/qoder/skills/wl-search/SKILL.md +149 -78
  210. package/templates/qoder/skills/wl-spec/SKILL.md +7 -5
  211. package/templates/qoder/skills/wl-status/SKILL.md +48 -16
  212. package/templates/qoder/skills/wl-task/SKILL.md +92 -10
  213. package/templates/qoder/skills/wl-test/SKILL.md +75 -29
  214. package/templates/qoder/templates/prd-full-template.md +7 -0
  215. package/templates/root/AGENTS.md +275 -236
  216. package/templates/root/requirements.txt +3 -0
  217. package/templates/root//344/275/277/347/224/250/350/257/264/346/230/216.md +3 -3
  218. package/templates/root//346/226/260/346/211/213/346/214/207/345/215/227.md +2 -2
  219. package/templates/.qoder/.runtime/ctx-cache-5660152f1d6dd819.md +0 -23
  220. package/templates/.qoder/.runtime/ctx-cache-afdce0dac06b25b0.md +0 -23
  221. package/templates/.qoder/.runtime/search-cache-eae7644e7b122f35.txt +0 -1
  222. package/templates/.qoder/learning/eval-history.jsonl +0 -28
  223. package/templates/data/index/wiki-index.json +0 -8
  224. package/templates/qoder/nul +0 -4
  225. package/templates/qoder/scripts/kg/extract.py +0 -398
  226. package/templates/qoder/scripts/kg/kg_semantic.py +0 -156
  227. package/templates/qoder/scripts/mcp/kg_mcp_server.py +0 -606
  228. package/templates/qoder/scripts/mcp/mcp_launcher.py +0 -442
  229. package/templates/qoder/scripts/mcp/zentao_mcp_server.py +0 -384
  230. package/templates/qoder/scripts/task/__init__.py +0 -11
  231. package/templates/qoder/scripts/test/__init__.py +0 -11
  232. package/templates/qoder/scripts/wlkj.py +0 -116
  233. /package/templates/qoder/scripts/{common → deployment}/__init__.py +0 -0
  234. /package/templates/qoder/scripts/{common → domain/kg/graph}/graph_traverse.py +0 -0
  235. /package/templates/qoder/scripts/{common → domain/kg/server}/repowiki.py +0 -0
  236. /package/templates/qoder/scripts/{common → foundation/io}/atomicio.py +0 -0
  237. /package/templates/qoder/scripts/{test → validation/test}/secure-ls.js +0 -0
@@ -1,536 +1,614 @@
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
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 domain.kg.extract.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
+ RE_PROXY_AJAX,
39
+ _resolve_drawer_api as _regex_resolve_drawer,
40
+ )
41
+
42
+
43
+ def _node_text(node, source_bytes):
44
+ """Get text of a tree-sitter node."""
45
+ return source_bytes[node.start_byte:node.end_byte].decode('utf-8', errors='replace')
46
+
47
+
48
+ def _find_calls_in_function_ast(func_node, source_bytes):
49
+ """Find all call_expression nodes inside a function body via AST.
50
+
51
+ Returns list of (callee_text, full_call_text).
52
+ """
53
+ calls = []
54
+ cursor = func_node.walk()
55
+ cursor.goto_first_child()
56
+ # Descend into body
57
+ def visit(node):
58
+ if node.type == 'call_expression':
59
+ callee = node.child_by_field_name('function')
60
+ if callee:
61
+ calls.append((_node_text(callee, source_bytes), _node_text(node, source_bytes)))
62
+ for child in node.children:
63
+ visit(child)
64
+ visit(func_node)
65
+ return calls
66
+
67
+
68
+ def ts_build_api_fn_table(api_dir):
69
+ """Tree-sitter version: build {fnName: {url, verb, file}} from api/*.ts.
70
+
71
+ Uses AST for exact function boundaries + call detection. Falls back to
72
+ regex version if tree-sitter unavailable.
73
+ """
74
+ if not _TS_AVAILABLE:
75
+ return regex_build_api_fn_table(api_dir)
76
+
77
+ table = {}
78
+ if not api_dir or not os.path.isdir(api_dir):
79
+ return table
80
+
81
+ for root, dirs, files in os.walk(api_dir):
82
+ dirs[:] = [d for d in dirs if d not in ('node_modules', 'dist', '__pycache__')]
83
+ for f in files:
84
+ if not f.endswith('.ts'):
85
+ continue
86
+ fpath = os.path.join(root, f)
87
+ try:
88
+ source = open(fpath, 'rb').read()
89
+ except Exception:
90
+ continue
91
+ urls = parse_api_enum_block(source.decode('utf-8', errors='replace'))
92
+
93
+ try:
94
+ tree = _PARSER.parse(source)
95
+ except Exception:
96
+ # Fallback to regex for this file
97
+ continue
98
+
99
+ # Walk AST to find export function / export const = () => {}
100
+ def visit(node):
101
+ if node.type == 'export_statement':
102
+ for child in node.children:
103
+ if child.type == 'function_declaration':
104
+ _process_fn(child, source, urls, fpath, table)
105
+ elif child.type == 'lexical_declaration':
106
+ # export const fnName = (...) => { ... }
107
+ _process_const_fn(child, source, urls, fpath, table)
108
+ elif node.type == 'function_declaration':
109
+ # Non-exported but still processable
110
+ pass
111
+ for child in node.children:
112
+ visit(child)
113
+
114
+ visit(tree.root_node)
115
+
116
+ # Merge with regex results (regex catches some TS-sitter misses)
117
+ regex_table = regex_build_api_fn_table(api_dir)
118
+ for k, v in regex_table.items():
119
+ if k not in table:
120
+ table[k] = v
121
+
122
+ return table
123
+
124
+
125
+ def _process_fn(fn_node, source, urls, fpath, table):
126
+ """Process a function_declaration node."""
127
+ name_node = fn_node.child_by_field_name('name')
128
+ if not name_node:
129
+ return
130
+ fn_name = _node_text(name_node, source)
131
+ body = fn_node.child_by_field_name('body')
132
+ if not body:
133
+ return
134
+ _extract_api_call(fn_name, body, source, urls, fpath, table)
135
+
136
+
137
+ def _process_const_fn(decl_node, source, urls, fpath, table):
138
+ """Process: export const fnName = (params) => { ... } or async."""
139
+ # Find the variable declarator
140
+ for child in decl_node.children:
141
+ if child.type == 'variable_declarator':
142
+ name_node = child.child_by_field_name('name')
143
+ value_node = child.child_by_field_name('value')
144
+ if not name_node or not value_node:
145
+ continue
146
+ fn_name = _node_text(name_node, source)
147
+ # Arrow function body
148
+ body = None
149
+ if value_node.type == 'arrow_function':
150
+ body = value_node.child_by_field_name('body')
151
+ elif value_node.type == 'function':
152
+ body = value_node.child_by_field_name('body')
153
+ if body:
154
+ _extract_api_call(fn_name, body, source, urls, fpath, table)
155
+
156
+
157
+ def _extract_api_call(fn_name, body_node, source, urls, fpath, table):
158
+ """Find requestClient calls inside a function body via AST."""
159
+ calls = _find_calls_in_function_ast(body_node, source)
160
+ entry = None
161
+ for callee, full_call in calls:
162
+ # requestClient.post<...>(Api.key, ...) or requestClient.get(...)
163
+ if callee.startswith('requestClient.'):
164
+ verb = callee.split('.')[-1]
165
+ # Extract first argument (Api.key or string literal)
166
+ m = re.search(r'requestClient\.\w+(?:<[^)]*?>)?\s*\(\s*(Api\.\w+|`[^`]*`|\'[^\']*\'|"[^"]*")', full_call)
167
+ if m:
168
+ url = _resolve_url_expr(m.group(1), urls)
169
+ if url:
170
+ entry = {'url': url, 'verb': verb, 'file': fpath.replace('\\', '/')}
171
+ break
172
+ # commonExport(Api.key, ...)
173
+ elif callee == 'commonExport':
174
+ m = re.search(r'commonExport\s*\(\s*(Api\.\w+)', full_call)
175
+ if m:
176
+ url = _resolve_url_expr(m.group(1), urls)
177
+ if url:
178
+ entry = {'url': url, 'verb': 'post', 'file': fpath.replace('\\', '/')}
179
+ break
180
+ if entry:
181
+ table[fn_name] = entry
182
+
183
+
184
+ def ts_get_fn_body_ast(content, fn_name):
185
+ """Get function body text via tree-sitter AST (exact boundaries).
186
+
187
+ More reliable than _get_fn_body's brace-counting for complex code.
188
+ """
189
+ if not _TS_AVAILABLE:
190
+ from domain.kg.extract.extract import _get_fn_body
191
+ return _get_fn_body(content, fn_name)
192
+
193
+ source = content.encode('utf-8')
194
+ try:
195
+ tree = _PARSER.parse(source)
196
+ except Exception:
197
+ from domain.kg.extract.extract import _get_fn_body
198
+ return _get_fn_body(content, fn_name)
199
+
200
+ root = tree.root_node
201
+
202
+ def find_fn(node):
203
+ if node.type in ('function_declaration', 'method_definition'):
204
+ name = node.child_by_field_name('name')
205
+ if name and _node_text(name, source) == fn_name:
206
+ body = node.child_by_field_name('body')
207
+ if body:
208
+ return _node_text(body, source)
209
+ for child in node.children:
210
+ result = find_fn(child)
211
+ if result:
212
+ return result
213
+ return None
214
+
215
+ body = find_fn(root)
216
+ if body:
217
+ return body
218
+ # Fallback: arrow function const
219
+ from domain.kg.extract.extract import _get_fn_body
220
+ return _get_fn_body(content, fn_name)
221
+
222
+
223
+ def _resolve_props_api_from_parent(parent_content, parent_path, drawer_map, drawer_var, api_table):
224
+ """Resolve props.api() by looking at the PARENT template.
225
+
226
+ When a drawer component's handler calls props.api(), the actual API
227
+ function was passed by the parent via :api="someFn" in the template.
228
+ This finds that binding and checks if someFn is a known api import.
229
+
230
+ Returns the api function name if found, else None.
231
+ """
232
+ # The drawer component tag name in template is the FIRST element of the
233
+ # destructured useVbenDrawer: const [CompTag, drawerVar] = useVbenDrawer(...)
234
+ # Find it: const [<CompTag>, drawerVar] = useVbenDrawer
235
+ comp_tag_m = re.search(
236
+ r"const\s+\[\s*(\w+)\s*,\s*" + re.escape(drawer_var) + r"\s*\]\s*=",
237
+ parent_content)
238
+ if not comp_tag_m:
239
+ return None
240
+ comp_tag = comp_tag_m.group(1)
241
+
242
+ # Find <CompTag ... :api="fnName" ... /> in the template
243
+ # Match :api="xxx" or :api="xxx" within a tag that starts with comp_tag
244
+ tag_pattern = re.compile(
245
+ r'<' + re.escape(comp_tag) + r'\b[^>]*?:api\s*=\s*["\'](\w+)["\']',
246
+ re.S)
247
+ m = tag_pattern.search(parent_content)
248
+ if not m:
249
+ # Also try kebab-case tag name (PascalCase kebab-case)
250
+ kebab = re.sub(r'([A-Z])', r'-\1', comp_tag).lower().lstrip('-')
251
+ tag_pattern2 = re.compile(
252
+ r'<' + re.escape(kebab) + r'\b[^>]*?:api\s*=\s*["\'](\w+)["\']',
253
+ re.S)
254
+ m = tag_pattern2.search(parent_content)
255
+ if not m:
256
+ return None
257
+
258
+ api_fn_name = m.group(1)
259
+ if api_fn_name in api_table:
260
+ return api_fn_name
261
+
262
+ # Maybe it's a wrapped call like :api="(data) => someFn(data)"
263
+ # Try to extract the function name from arrow expressions
264
+ inner_fn = re.search(r'\b(\w+)\s*\(', api_fn_name)
265
+ if inner_fn and inner_fn.group(1) in api_table:
266
+ return inner_fn.group(1)
267
+
268
+ return None
269
+
270
+
271
+ def _resolve_composable_handler(vue_content, vue_path, handler_name, api_table):
272
+ """Resolve a handler that comes from a composable (useXxx()).
273
+
274
+ Pattern: const { handleAdd } = useLeftBoxUser()
275
+ find the composable import path open it find handleAdd's definition.
276
+ Returns the function body text, or '' if not found.
277
+ """
278
+ # Find: const { ... handlerName ... } = useXxx()
279
+ m = re.search(
280
+ r'const\s*\{([^}]*\b' + re.escape(handler_name) + r'\b[^}]*)\}\s*=\s*use(\w+)\s*\(',
281
+ vue_content)
282
+ if not m:
283
+ return ''
284
+ composable_fn = 'use' + m.group(2) # e.g. useLeftBoxUser
285
+
286
+ # Find import of useXxx from '...'
287
+ imp_m = re.search(
288
+ r'import\s+\{[^}]*\b' + re.escape(composable_fn) + r'\b[^}]*\}\s+from\s+[\'"]([^\'"]+)[\'"]',
289
+ vue_content)
290
+ if not imp_m:
291
+ return ''
292
+ import_path = imp_m.group(1)
293
+
294
+ # Resolve import path to file
295
+ dirname = os.path.dirname(vue_path)
296
+ if import_path.startswith('#/'):
297
+ # Alias: #/ = src/
298
+ src_base = vue_path.replace('\\', '/').split('/src/')[0] + '/src/'
299
+ comp_path = os.path.normpath(os.path.join(src_base, import_path.replace('#/', '')))
300
+ elif import_path.startswith('./') or import_path.startswith('../'):
301
+ comp_path = os.path.normpath(os.path.join(dirname, import_path))
302
+ else:
303
+ return ''
304
+
305
+ # Try .ts extension if not present
306
+ if not os.path.isfile(comp_path):
307
+ for ext in ('.ts', '.vue'):
308
+ if os.path.isfile(comp_path + ext):
309
+ comp_path += ext
310
+ break
311
+
312
+ if not os.path.isfile(comp_path):
313
+ return ''
314
+
315
+ try:
316
+ comp_content = open(comp_path, encoding='utf-8', errors='ignore').read()
317
+ except Exception:
318
+ return ''
319
+
320
+ # Find handler_name's definition in composable (function or const arrow)
321
+ return ts_get_fn_body_ast(comp_content, handler_name) or _regex_fn_body(comp_content, handler_name)
322
+
323
+
324
+ def _regex_fn_body(content, fn_name, max_len=500):
325
+ """Regex fallback for function body (for composable files tree-sitter may miss)."""
326
+ from domain.kg.extract.extract import _get_fn_body
327
+ return _get_fn_body(content, fn_name, max_len)
328
+
329
+
330
+ def ts_trace_clicks(vue_path, vue_content, api_table):
331
+ """Tree-sitter enhanced click tracing.
332
+
333
+ Uses AST for function body extraction (more precise than regex).
334
+ The drawer/modal/router logic stays as regex (it's application-level,
335
+ not parse-level). Only the fn_body extraction is upgraded.
336
+ """
337
+ if not _TS_AVAILABLE:
338
+ return regex_trace_clicks(vue_path, vue_content, api_table)
339
+
340
+ dirname = os.path.dirname(vue_path)
341
+
342
+ # 1. Collect api imports (same as regex)
343
+ api_imports = set()
344
+ for im in RE_API_IMPORT.finditer(vue_content):
345
+ for name in im.group(1).split(','):
346
+ name = name.strip().split(' as ')[0].strip()
347
+ if name and name in api_table:
348
+ api_imports.add(name)
349
+
350
+ # 2. Find all @click handlers + button text (same as regex)
351
+ clicks = list(RE_CLICK.finditer(vue_content))
352
+ if not clicks:
353
+ return []
354
+
355
+ # 3. Build drawer map (E2 fix: also resolve #/ and @/ alias imports,
356
+ # not just local ./xxx.vue many ICS drawers are imported via
357
+ # #/views/... alias paths which the original RE_LOCAL_VUE_IMPORT missed)
358
+ drawer_map = {}
359
+ # Collect ALL vue imports (local + alias + defineAsyncComponent) into a name→path map
360
+ all_vue_imports = {} # {import_name: import_path}
361
+ for im in RE_LOCAL_VUE_IMPORT.finditer(vue_content):
362
+ all_vue_imports[im.group(1)] = im.group(2)
363
+ # Also match alias imports: import xxx from '#/views/....vue' or '@/views/....vue'
364
+ for im in re.finditer(
365
+ r"import\s+(\w+)\s+from\s+['\"](?:#|@)/views/([^'\"]+\.vue)['\"]",
366
+ vue_content):
367
+ all_vue_imports[im.group(1)] = '#/views/' + im.group(2)
368
+ # Also match: const xxx = defineAsyncComponent(() => import('./xxx.vue'))
369
+ # — ICS uses this pattern extensively for drawer components
370
+ for im in re.finditer(
371
+ r"(?:const|let)\s+(\w+)\s*=\s*defineAsyncComponent\s*\(\s*\(\)\s*=>\s*import\s*\(\s*['\"]([^'\"]+\.vue)['\"]\s*\)\s*\)",
372
+ vue_content):
373
+ all_vue_imports[im.group(1)] = im.group(2)
374
+
375
+ for dm in re.finditer(
376
+ r"const\s+\[\s*\w+\s*,\s*(\w+)\s*\]\s*=\s*useVben(?:Drawer|Modal)\s*\(\s*\{([^}]*)\}",
377
+ vue_content, re.S):
378
+ drawer_var = dm.group(1)
379
+ decl_body = dm.group(2)
380
+ cm = re.search(r"connectedComponent:\s*(\w+)", decl_body)
381
+ if not cm:
382
+ continue
383
+ comp_import_name = cm.group(1)
384
+ if comp_import_name in all_vue_imports:
385
+ drawer_map[drawer_var] = all_vue_imports[comp_import_name]
386
+
387
+ traces = []
388
+
389
+ for cm in clicks:
390
+ handler_expr = cm.group(1).strip()
391
+ btn_text = ''
392
+ tm = RE_BTN_TEXT_AFTER_CLICK.search(vue_content, cm.start())
393
+ if tm:
394
+ btn_text = re.sub(r'\{\{[^}]*\}\}', '', tm.group(1).strip())[:12]
395
+
396
+ if handler_expr.startswith('$') or '.splice(' in handler_expr or '.push(' in handler_expr:
397
+ continue
398
+
399
+ handler_name = handler_expr.split('(')[0].split('.')[0].strip()
400
+ if not handler_name or handler_name in ('undefined', '', 'true', 'false'):
401
+ continue
402
+ if handler_name in ('item', 'emit', 'row', 'data') and '.' not in handler_expr:
403
+ continue
404
+
405
+ # Use tree-sitter for function body (the E2 upgrade)
406
+ fn_body = ts_get_fn_body_ast(vue_content, handler_name)
407
+
408
+ trace = {
409
+ 'vue_file': vue_path.replace('\\', '/'),
410
+ 'button': btn_text,
411
+ 'handler': handler_name,
412
+ 'api_fn': None, 'endpoint': None, 'verb': None,
413
+ 'confidence': None,
414
+ }
415
+
416
+ # Pattern B: direct api call
417
+ hits = _find_api_calls_in_text(fn_body, api_table, api_imports)
418
+ if hits:
419
+ fn, url, verb = hits[0]
420
+ trace.update(api_fn=fn, endpoint=url, verb=verb, confidence='high')
421
+ traces.append(trace)
422
+ continue
423
+
424
+ # Pattern G: proxyConfig.ajax({ url: '...' }) — ICS 标准 VxeGrid 代理模式
425
+ # 很多 ICS 表格页查询不走 requestClient, 而是直接在 proxyConfig.ajax 里
426
+ # url。handler 可能是 reload/query 调用 grid 刷新, 触发 proxyConfig。
427
+ # 也匹配整个文件级别的 proxyConfig (handler 间接触发)
428
+ proxy_hit = _match_proxy_ajax(fn_body, vue_content, api_table)
429
+ if proxy_hit:
430
+ fn, url, verb = proxy_hit
431
+ trace.update(api_fn=fn, endpoint=url, verb=verb, confidence='medium')
432
+ traces.append(trace)
433
+ continue
434
+
435
+ # Pattern D: drawer open
436
+ open_m = RE_DRAWER_OPEN.search(fn_body)
437
+ if open_m:
438
+ drawer_var = open_m.group(1)
439
+ comp_rel = drawer_map.get(drawer_var)
440
+ if comp_rel:
441
+ # Resolve component path: local ./xxx.vue, alias #/views/..., or #/components/...
442
+ vue_path_norm = vue_path.replace('\\', '/') # normalize for split
443
+ if comp_rel.startswith('#/views/'):
444
+ alias_path = comp_rel.replace('#/views/', '')
445
+ src_base = vue_path_norm.split('/src/views/')[0] + '/src/views/'
446
+ comp_path = os.path.normpath(os.path.join(src_base, alias_path))
447
+ elif comp_rel.startswith('#/components/') or comp_rel.startswith('@/components/'):
448
+ alias_path = comp_rel.replace('#/components/', '').replace('@/components/', '')
449
+ src_base = vue_path_norm.split('/src/')[0] + '/src/components/'
450
+ comp_path = os.path.normpath(os.path.join(src_base, alias_path))
451
+ else:
452
+ comp_path = os.path.normpath(os.path.join(dirname, comp_rel))
453
+ if os.path.isfile(comp_path):
454
+ try:
455
+ comp_content = open(comp_path, encoding='utf-8', errors='ignore').read()
456
+ comp_api_imports = set()
457
+ for im in RE_API_IMPORT.finditer(comp_content):
458
+ for name in im.group(1).split(','):
459
+ name = name.strip().split(' as ')[0].strip()
460
+ if name in api_table:
461
+ comp_api_imports.add(name)
462
+ resolved = False
463
+ # First try: find a confirm handler that directly calls api
464
+ for confirm_name in ('handleConfirm', 'onConfirm', 'handleSubmit', 'onOk'):
465
+ cbody = ts_get_fn_body_ast(comp_content, confirm_name)
466
+ if cbody:
467
+ chits = _find_api_calls_in_text(cbody, api_table, comp_api_imports)
468
+ if chits:
469
+ fn, url, verb = chits[0]
470
+ trace.update(api_fn=fn, endpoint=url, verb=verb, confidence='medium')
471
+ resolved = True
472
+ break
473
+
474
+ # Second try: if component uses props.api ANYWHERE (not just
475
+ # in confirm handler — could be in grid query, watch, etc.),
476
+ # resolve via parent template :api="xxx" binding.
477
+ # This is the key insight: don't search for WHICH handler calls
478
+ # props.api just check IF props.api exists, then find the API
479
+ # from the parent. Humans do exactly this.
480
+ if not resolved and 'props.' in comp_content and re.search(
481
+ r'props\.\w+\s*\(', comp_content):
482
+ parent_api_fn = _resolve_props_api_from_parent(
483
+ vue_content, vue_path, drawer_map, drawer_var, api_table)
484
+ if parent_api_fn:
485
+ info = api_table[parent_api_fn]
486
+ trace.update(api_fn=parent_api_fn, endpoint=info['url'],
487
+ verb=info.get('verb', '?'), confidence='medium')
488
+ resolved = True
489
+
490
+ # Third try: component imports API functions directly (no
491
+ # props.api) — e.g. a selector drawer that calls getList in
492
+ # its grid query. Take the first imported api fn as the
493
+ # associated endpoint (lower confidence — we know the drawer
494
+ # USES this API but can't pinpoint which button triggers it).
495
+ if not resolved and comp_api_imports:
496
+ first_fn = sorted(comp_api_imports)[0]
497
+ info = api_table[first_fn]
498
+ trace.update(api_fn=first_fn, endpoint=info['url'],
499
+ verb=info.get('verb', '?'), confidence='low')
500
+ resolved = True
501
+ except Exception:
502
+ pass
503
+ if trace['endpoint']:
504
+ traces.append(trace)
505
+ continue
506
+
507
+ # Modal.confirm
508
+ mc_m = RE_MODAL_CONFIRM.search(fn_body)
509
+ if mc_m:
510
+ onok_body = fn_body[mc_m.end():]
511
+ hits = _find_api_calls_in_text(onok_body[:400], api_table, api_imports)
512
+ if hits:
513
+ fn, url, verb = hits[0]
514
+ trace.update(api_fn=fn, endpoint=url, verb=verb, confidence='low')
515
+ traces.append(trace)
516
+ continue
517
+
518
+ # router.push
519
+ if fn_body and RE_ROUTER_PUSH.search(fn_body):
520
+ trace['confidence'] = 'deferred'
521
+ traces.append(trace)
522
+ continue
523
+
524
+ # Pattern E: imported function used directly as @click handler
525
+ # (e.g. @click="toContract" where toContract is an imported api fn)
526
+ if not fn_body and handler_name in api_table:
527
+ info = api_table[handler_name]
528
+ trace.update(api_fn=handler_name, endpoint=info['url'],
529
+ verb=info.get('verb', '?'), confidence='high')
530
+ traces.append(trace)
531
+ continue
532
+
533
+ # Pattern F: composable-returned method
534
+ # (e.g. const { handleAdd } = useLeftBoxUser())
535
+ if not fn_body:
536
+ comp_body = _resolve_composable_handler(vue_content, vue_path, handler_name, api_table)
537
+ if comp_body:
538
+ hits = _find_api_calls_in_text(comp_body, api_table, set(api_table.keys()))
539
+ if hits:
540
+ fn, url, verb = hits[0]
541
+ trace.update(api_fn=fn, endpoint=url, verb=verb, confidence='medium')
542
+ traces.append(trace)
543
+ continue
544
+
545
+ # Pattern H: workflow_submit — ICS 标准审批流提交组合式函数
546
+ # handler 调用 workflow_submit/submitWorkflow/workflowSubmit 时, 它内部
547
+ # 会发 POST 到审批流后端。静态无法解析具体 URL, 但标记为提交流程,
548
+ # confidence=medium, 避免这些 handler 被误判为"无 API 调用"。
549
+ if fn_body and re.search(
550
+ r'\b(?:workflow[_-]?submit|submit[_-]?workflow|workflowSubmit|submitWorkflow)\s*\(',
551
+ fn_body):
552
+ trace.update(endpoint='[workflow-submit]', verb='POST',
553
+ api_fn='workflow_submit', confidence='medium')
554
+ traces.append(trace)
555
+ continue
556
+
557
+ trace['confidence'] = 'unresolved'
558
+ traces.append(trace)
559
+
560
+ return traces
561
+
562
+
563
+ def _match_proxy_ajax(fn_body, vue_content, api_table):
564
+ """Pattern G: 从 proxyConfig.ajax.query 函数体中提取 API 调用。
565
+
566
+ ICS 真实写法 (审核 601 个文件确认):
567
+ proxyConfig: { ajax: { query: async ({page}, formValues) => {
568
+ return await getUnconventionalDetailList({...}) ← 导入的 api 函数
569
+ }}}
570
+ query 函数体里调用的是已导入的 api 函数 (在 api_table 里),
571
+ 用 _find_api_calls_in_text 匹配即可。这是页面级数据加载入口,
572
+ 不属于某个按钮 handler, 但任何触发表格 reload 的按钮都间接走它。
573
+
574
+ Returns:
575
+ (api_fn, url, verb) 或 None
576
+ """
577
+ # proxyConfig 在 <script setup> 顶层, 不在 handler 函数体内 → 扫整个文件
578
+ text = vue_content
579
+ if not text or not api_table:
580
+ return None
581
+ # 收集该文件所有导入的 api 函数名 (proxyConfig 里的 query 可能用任意导入的 fn)
582
+ all_imports = set(api_table.keys())
583
+ for im in RE_API_IMPORT.finditer(text):
584
+ for name in im.group(1).split(','):
585
+ name = name.strip().split(' as ')[0].strip()
586
+ if name and name in api_table:
587
+ all_imports.add(name)
588
+ for pm in RE_PROXY_AJAX.finditer(text):
589
+ # 提取 query 函数体: 从 'async (' 后做花括号匹配
590
+ start = pm.end() - 1 # 回到 '(' 位置
591
+ # 找到 '=>' 后的第一个 '{'
592
+ arrow_idx = text.find('=>', pm.end())
593
+ if arrow_idx < 0:
594
+ continue
595
+ brace_idx = text.find('{', arrow_idx)
596
+ if brace_idx < 0:
597
+ continue
598
+ # 花括号深度匹配提取函数体
599
+ depth = 1
600
+ i = brace_idx + 1
601
+ end = min(len(text), brace_idx + 800) # 最多取 800 字符
602
+ while i < end and depth > 0:
603
+ if text[i] == '{':
604
+ depth += 1
605
+ elif text[i] == '}':
606
+ depth -= 1
607
+ i += 1
608
+ query_body = text[brace_idx + 1:i]
609
+ # 在 query 函数体里找 api 调用
610
+ hits = _find_api_calls_in_text(query_body, api_table, all_imports)
611
+ if hits:
612
+ fn, url, verb = hits[0]
613
+ return (fn, url, verb or 'GET')
614
+ return None