@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.
- package/bin/cli.js +1435 -1373
- package/package.json +1 -1
- package/templates/qoder/commands/optional/wl-insight.md +6 -4
- package/templates/qoder/commands/optional/wl-report.md +20 -39
- package/templates/qoder/commands/optional/wl-status.md +4 -4
- package/templates/qoder/commands/wl-code.md +2 -2
- package/templates/qoder/commands/wl-design.md +5 -5
- package/templates/qoder/commands/wl-init.md +3 -3
- package/templates/qoder/commands/wl-prd.md +26 -13
- package/templates/qoder/commands/wl-req.md +3 -3
- package/templates/qoder/commands/wl-search.md +78 -47
- package/templates/qoder/commands/wl-task.md +343 -40
- package/templates/qoder/commands/wl-test.md +10 -10
- package/templates/qoder/config.yaml +35 -13
- package/templates/qoder/hooks/post-tool-use.py +181 -0
- package/templates/qoder/hooks/session-start.py +82 -1
- package/templates/qoder/hooks/stop-eval.py +207 -0
- package/templates/qoder/hooks/user-prompt-submit.py +139 -0
- package/templates/qoder/rules/wl-pipeline.md +70 -8
- package/templates/qoder/scripts/README.md +78 -115
- package/templates/qoder/scripts/capability/__init__.py +26 -0
- package/templates/qoder/scripts/capability/__main__.py +72 -0
- package/templates/qoder/scripts/capability/adapters/__init__.py +29 -0
- package/templates/qoder/scripts/capability/adapters/cli.py +316 -0
- package/templates/qoder/scripts/capability/adapters/mcp.py +334 -0
- package/templates/qoder/scripts/capability/adapters/qw.py +271 -0
- package/templates/qoder/scripts/capability/caps/__init__.py +27 -0
- package/templates/qoder/scripts/capability/caps/context.py +36 -0
- package/templates/qoder/scripts/capability/caps/cron.py +50 -0
- package/templates/qoder/scripts/capability/caps/identity.py +43 -0
- package/templates/qoder/scripts/capability/caps/memory.py +71 -0
- package/templates/qoder/scripts/capability/caps/notify.py +41 -0
- package/templates/qoder/scripts/capability/caps/present.py +48 -0
- package/templates/qoder/scripts/capability/caps/repo.py +31 -0
- package/templates/qoder/scripts/capability/caps/sandbox.py +43 -0
- package/templates/qoder/scripts/capability/chain.py +92 -0
- package/templates/qoder/scripts/capability/memory_chain.py +41 -0
- package/templates/qoder/scripts/capability/registry.py +246 -0
- package/templates/qoder/scripts/capability/registry_mcp.py +250 -0
- package/templates/qoder/scripts/capability/smoke_test.py +211 -0
- package/templates/qoder/scripts/capability/smoke_test_report.json +94 -0
- package/templates/qoder/scripts/{report → deployment/setup}/__init__.py +11 -11
- package/templates/qoder/scripts/{setup → deployment/setup}/carriers.py +33 -26
- package/templates/qoder/scripts/deployment/setup/carriers_verify.py +148 -0
- package/templates/qoder/scripts/{setup → deployment/setup}/init_doctor.py +40 -16
- package/templates/qoder/scripts/{setup → deployment/setup}/install_qoderwork.py +20 -12
- package/templates/qoder/scripts/{setup → deployment/setup}/platform_doctor.py +8 -2
- package/templates/qoder/scripts/{setup → deployment/setup}/repo_root.py +113 -112
- package/templates/qoder/scripts/{setup → deployment/setup}/setup.py +31 -11
- package/templates/qoder/scripts/{setup → deployment/setup}/setup_lanhu.py +13 -7
- package/templates/qoder/scripts/domain/__init__.py +0 -0
- package/templates/qoder/scripts/domain/design/__init__.py +0 -0
- package/templates/qoder/scripts/{report → domain/design}/fill_prototype.py +12 -6
- package/templates/qoder/scripts/{report → domain/design}/gen_design_doc.py +8 -2
- package/templates/qoder/scripts/{setup → domain/kg}/__init__.py +11 -11
- package/templates/qoder/scripts/domain/kg/build/__init__.py +0 -0
- package/templates/qoder/scripts/{kg → domain/kg/build}/build_entity_registry.py +7 -2
- package/templates/qoder/scripts/{kg → domain/kg/build}/build_relations.py +7 -2
- package/templates/qoder/scripts/{kg → domain/kg/build}/build_style_index.py +13 -7
- package/templates/qoder/scripts/{kg → domain/kg/build}/build_workflows.py +7 -2
- package/templates/qoder/scripts/{kg → domain/kg/build}/kg_build.py +76 -9
- package/templates/qoder/scripts/{kg → domain/kg/build}/kg_build_db.py +7 -2
- package/templates/qoder/scripts/{kg → domain/kg/build}/kg_incremental.py +11 -6
- package/templates/qoder/scripts/{report → domain/kg/build}/learn_aggregate.py +9 -4
- package/templates/qoder/scripts/domain/kg/extract/__init__.py +0 -0
- package/templates/qoder/scripts/{common → domain/kg/extract}/extract.py +430 -419
- package/templates/qoder/scripts/{common → domain/kg/extract}/test_extract.py +115 -115
- package/templates/qoder/scripts/{common → domain/kg/extract}/ts_extract.py +614 -536
- package/templates/qoder/scripts/domain/kg/graph/__init__.py +0 -0
- package/templates/qoder/scripts/{kg → domain/kg/graph}/kg_link_db.py +7 -2
- package/templates/qoder/scripts/domain/kg/graph/kg_semantic.py +683 -0
- package/templates/qoder/scripts/{kg → domain/kg}/kg.py +263 -30
- package/templates/qoder/scripts/{common → domain/kg}/kg_capabilities.py +182 -182
- package/templates/qoder/scripts/domain/kg/search/__init__.py +0 -0
- package/templates/qoder/scripts/{kg → domain/kg/search}/context_pack.py +30 -9
- package/templates/qoder/scripts/{kg → domain/kg/search}/enrich_prompt.py +10 -4
- package/templates/qoder/scripts/{kg → domain/kg/search}/prefetch.py +33 -8
- package/templates/qoder/scripts/{common → domain/kg/search}/search_engine.py +207 -205
- package/templates/qoder/scripts/{kg → domain/kg/search}/search_index.py +143 -77
- package/templates/qoder/scripts/domain/kg/server/__init__.py +0 -0
- package/templates/qoder/scripts/domain/kg/server/kgd.py +549 -0
- package/templates/qoder/scripts/domain/kg/server/perf_bench.py +197 -0
- package/templates/qoder/scripts/domain/kg/storage/__init__.py +0 -0
- package/templates/qoder/scripts/{kg → domain/kg/storage}/kg_duckdb.py +38 -14
- package/templates/qoder/scripts/domain/learning/__init__.py +0 -0
- package/templates/qoder/scripts/{report → domain/learning}/learn.py +8 -3
- package/templates/qoder/scripts/{mcp → domain/report}/__init__.py +11 -11
- package/templates/qoder/scripts/{report → domain/report}/add_session.py +9 -3
- package/templates/qoder/scripts/{report → domain/report}/export.py +10 -5
- package/templates/qoder/scripts/{report → domain/report}/report.py +9 -4
- package/templates/qoder/scripts/domain/report/report_snapshot.py +261 -0
- package/templates/qoder/scripts/{report → domain/report}/role.py +10 -4
- package/templates/qoder/scripts/{report → domain/report}/status.py +112 -9
- package/templates/qoder/scripts/domain/report/status_snapshot.py +191 -0
- package/templates/qoder/scripts/domain/requirement/__init__.py +0 -0
- package/templates/qoder/scripts/{report → domain/requirement}/archive_prd.py +8 -2
- package/templates/qoder/scripts/{report → domain/requirement}/req.py +228 -222
- package/templates/qoder/scripts/{kg → domain/task}/__init__.py +11 -11
- package/templates/qoder/scripts/{task → domain/task}/git_sync.py +40 -21
- package/templates/qoder/scripts/{task → domain/task}/syncgate.py +13 -8
- package/templates/qoder/scripts/{task → domain/task}/task.py +13 -5
- package/templates/qoder/scripts/{task → domain/task}/task_lifecycle.py +606 -596
- package/templates/qoder/scripts/{task → domain/task}/task_query.py +162 -161
- package/templates/qoder/scripts/{task → domain/task}/task_relations.py +425 -424
- package/templates/qoder/scripts/{task → domain/task}/team_sync.py +9 -4
- package/templates/qoder/scripts/domain/task/zentao_sync.py +370 -0
- package/templates/qoder/scripts/foundation/__init__.py +0 -0
- package/templates/qoder/scripts/{common → foundation}/bootstrap.py +10 -10
- package/templates/qoder/scripts/foundation/core/__init__.py +0 -0
- package/templates/qoder/scripts/{common → foundation/core}/cmd_registry.py +1 -1
- package/templates/qoder/scripts/{common → foundation/core}/config.py +1 -1
- package/templates/qoder/scripts/{common → foundation/core}/paths.py +706 -586
- package/templates/qoder/scripts/foundation/data/__init__.py +0 -0
- package/templates/qoder/scripts/{common → foundation/data}/contract.py +2 -2
- package/templates/qoder/scripts/{common → foundation/data}/events.py +2 -2
- package/templates/qoder/scripts/{common → foundation/data}/result.py +1 -1
- package/templates/qoder/scripts/{common → foundation/data}/task_utils.py +427 -404
- package/templates/qoder/scripts/foundation/identity/__init__.py +0 -0
- package/templates/qoder/scripts/{common → foundation/identity}/check_publish.py +103 -98
- package/templates/qoder/scripts/{common → foundation/identity}/developer.py +239 -239
- package/templates/qoder/scripts/{common → foundation/identity}/guard.py +2 -2
- package/templates/qoder/scripts/{common → foundation/identity}/identity.py +14 -10
- package/templates/qoder/scripts/{common → foundation/identity}/roles.py +1 -1
- package/templates/qoder/scripts/foundation/integrations/__init__.py +0 -0
- package/templates/qoder/scripts/{common → foundation/integrations}/active_task.py +30 -15
- package/templates/qoder/scripts/{common → foundation/integrations}/feishu.py +2 -2
- package/templates/qoder/scripts/{common → foundation/integrations}/terms.py +3 -3
- package/templates/qoder/scripts/foundation/integrations/zentao_client.py +220 -0
- package/templates/qoder/scripts/foundation/io/__init__.py +0 -0
- package/templates/qoder/scripts/{common → foundation/io}/filelock.py +1 -1
- package/templates/qoder/scripts/{common → foundation/io}/reqid.py +1 -1
- package/templates/qoder/scripts/foundation/protocol/__init__.py +0 -0
- package/templates/qoder/scripts/foundation/protocol/duckdb_conn.py +39 -0
- package/templates/qoder/scripts/{common → foundation/protocol}/mcp_base.py +1 -1
- package/templates/qoder/scripts/foundation/utils/__init__.py +0 -0
- package/templates/qoder/scripts/{common → foundation/utils}/eval_api.py +12 -1
- package/templates/qoder/scripts/{common → foundation/utils}/pip_install.py +23 -6
- package/templates/qoder/scripts/{common → foundation/utils}/platform_guard.py +1 -1
- package/templates/qoder/scripts/orchestration/__init__.py +0 -0
- package/templates/qoder/scripts/orchestration/wlkj.py +185 -0
- package/templates/qoder/scripts/protocol/__init__.py +0 -0
- package/templates/qoder/scripts/protocol/browser/README.md +207 -0
- package/templates/qoder/scripts/protocol/browser/SKILL.md +265 -0
- package/templates/qoder/scripts/protocol/browser/config.env +9 -0
- package/templates/qoder/scripts/protocol/browser/references/cdp-api.md +110 -0
- package/templates/qoder/scripts/protocol/browser/references/migration-2.5.3.md +72 -0
- package/templates/qoder/scripts/protocol/browser/references/site-patterns/.gitkeep +0 -0
- package/templates/qoder/scripts/protocol/browser/scripts/browser-discovery.mjs +138 -0
- package/templates/qoder/scripts/protocol/browser/scripts/cdp-proxy.mjs +672 -0
- package/templates/qoder/scripts/protocol/browser/scripts/check-deps.mjs +206 -0
- package/templates/qoder/scripts/protocol/browser/scripts/find-url.mjs +253 -0
- package/templates/qoder/scripts/protocol/browser/scripts/match-site.mjs +46 -0
- package/templates/qoder/scripts/protocol/browser/templates/config.env.template +9 -0
- package/templates/qoder/scripts/protocol/mcp/__init__.py +11 -0
- package/templates/qoder/scripts/protocol/mcp/kg_mcp_server.py +478 -0
- package/templates/qoder/scripts/{mcp → protocol/mcp}/lanhu_stdio_wrapper.py +11 -5
- package/templates/qoder/scripts/{mcp → protocol/mcp}/mcp_doctor.py +47 -14
- package/templates/qoder/scripts/protocol/mcp/mcp_launcher.py +600 -0
- package/templates/qoder/scripts/{mcp → protocol/mcp}/mysql_mcp_server.py +181 -67
- package/templates/qoder/scripts/protocol/mcp/zentao_mcp_server.py +1920 -0
- package/templates/qoder/scripts/protocol/transports/__init__.py +56 -0
- package/templates/qoder/scripts/protocol/transports/base.py +87 -0
- package/templates/qoder/scripts/protocol/transports/cli.py +132 -0
- package/templates/qoder/scripts/protocol/transports/http.py +141 -0
- package/templates/qoder/scripts/protocol/transports/stdio.py +293 -0
- package/templates/qoder/scripts/run_weekly_update.bat +27 -18
- package/templates/qoder/scripts/run_weekly_update.sh +22 -13
- package/templates/qoder/scripts/validation/__init__.py +0 -0
- package/templates/qoder/scripts/validation/eval/__init__.py +0 -0
- package/templates/qoder/scripts/validation/eval/perf-report-2026-06-24.md +139 -0
- package/templates/qoder/scripts/validation/eval/qwork_harness.py +617 -0
- package/templates/qoder/scripts/validation/eval/report-commands.md +220 -0
- package/templates/qoder/scripts/validation/eval/transcript_timing.py +386 -0
- package/templates/qoder/scripts/validation/metrics/__init__.py +0 -0
- package/templates/qoder/scripts/{report → validation/metrics}/eval_prd.py +17 -10
- package/templates/qoder/scripts/validation/metrics/usability_score.py +750 -0
- package/templates/qoder/scripts/validation/test/__init__.py +11 -0
- package/templates/qoder/scripts/validation/test/assertion_gen.py +551 -0
- package/templates/qoder/scripts/{test → validation/test}/autotest.py +64 -4
- package/templates/qoder/scripts/{common → validation/test}/autotest_auth.py +1 -1
- package/templates/qoder/scripts/{test → validation/test}/autotest_batch.py +16 -3
- package/templates/qoder/scripts/{test → validation/test}/autotest_data.py +17 -12
- package/templates/qoder/scripts/validation/test/autotest_opencli.py +646 -0
- package/templates/qoder/scripts/{test → validation/test}/autotest_run.py +17 -3
- package/templates/qoder/scripts/validation/test/autotest_webaccess.py +571 -0
- package/templates/qoder/scripts/{test → validation/test}/benchmark.py +12 -6
- package/templates/qoder/scripts/{test → validation/test}/kg_auto_login.py +8 -2
- package/templates/qoder/scripts/{test → validation/test}/kg_test_runner.py +7 -2
- package/templates/qoder/scripts/{test → validation/test}/page_probe.py +8 -3
- package/templates/qoder/scripts/validation/test/smoke_all_commands.py +170 -0
- package/templates/qoder/settings.json +30 -0
- package/templates/qoder/skills/design-import/SKILL.md +22 -13
- package/templates/qoder/skills/design-review/SKILL.md +19 -10
- package/templates/qoder/skills/prd-generator/SKILL.md +22 -18
- package/templates/qoder/skills/prd-review/SKILL.md +6 -4
- package/templates/qoder/skills/prototype-generator/SKILL.md +63 -14
- package/templates/qoder/skills/spec-coder/SKILL.md +22 -13
- package/templates/qoder/skills/spec-generator/SKILL.md +22 -10
- package/templates/qoder/skills/test-generator/SKILL.md +7 -5
- package/templates/qoder/skills/wl-code/SKILL.md +23 -11
- package/templates/qoder/skills/wl-commit/SKILL.md +9 -8
- package/templates/qoder/skills/wl-design/SKILL.md +5 -3
- package/templates/qoder/skills/wl-init/SKILL.md +6 -6
- package/templates/qoder/skills/wl-insight/SKILL.md +23 -10
- package/templates/qoder/skills/wl-prd-full/SKILL.md +57 -6
- package/templates/qoder/skills/wl-prd-quick/SKILL.md +3 -3
- package/templates/qoder/skills/wl-prd-review/SKILL.md +16 -4
- package/templates/qoder/skills/wl-report/SKILL.md +25 -23
- package/templates/qoder/skills/wl-search/SKILL.md +149 -78
- package/templates/qoder/skills/wl-spec/SKILL.md +7 -5
- package/templates/qoder/skills/wl-status/SKILL.md +48 -16
- package/templates/qoder/skills/wl-task/SKILL.md +92 -10
- package/templates/qoder/skills/wl-test/SKILL.md +75 -29
- package/templates/qoder/templates/prd-full-template.md +7 -0
- package/templates/root/AGENTS.md +275 -236
- package/templates/root/requirements.txt +3 -0
- package/templates/root//344/275/277/347/224/250/350/257/264/346/230/216.md +3 -3
- package/templates/root//346/226/260/346/211/213/346/214/207/345/215/227.md +2 -2
- package/templates/.qoder/.runtime/ctx-cache-5660152f1d6dd819.md +0 -23
- package/templates/.qoder/.runtime/ctx-cache-afdce0dac06b25b0.md +0 -23
- package/templates/.qoder/.runtime/search-cache-eae7644e7b122f35.txt +0 -1
- package/templates/.qoder/learning/eval-history.jsonl +0 -28
- package/templates/data/index/wiki-index.json +0 -8
- package/templates/qoder/nul +0 -4
- package/templates/qoder/scripts/kg/extract.py +0 -398
- package/templates/qoder/scripts/kg/kg_semantic.py +0 -156
- package/templates/qoder/scripts/mcp/kg_mcp_server.py +0 -606
- package/templates/qoder/scripts/mcp/mcp_launcher.py +0 -442
- package/templates/qoder/scripts/mcp/zentao_mcp_server.py +0 -384
- package/templates/qoder/scripts/task/__init__.py +0 -11
- package/templates/qoder/scripts/test/__init__.py +0 -11
- package/templates/qoder/scripts/wlkj.py +0 -116
- /package/templates/qoder/scripts/{common → deployment}/__init__.py +0 -0
- /package/templates/qoder/scripts/{common → domain/kg/graph}/graph_traverse.py +0 -0
- /package/templates/qoder/scripts/{common → domain/kg/server}/repowiki.py +0 -0
- /package/templates/qoder/scripts/{common → foundation/io}/atomicio.py +0 -0
- /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
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
cursor.
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
for
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
calls
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
#
|
|
233
|
-
#
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
#
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
re.
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
kebab
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
re.
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
#
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
if not
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
#
|
|
356
|
-
#
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
#
|
|
369
|
-
for
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
'
|
|
410
|
-
'
|
|
411
|
-
'
|
|
412
|
-
'
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
#
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
#
|
|
479
|
-
#
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
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 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
|