@hupan56/wlkj 2.7.11 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/bin/cli.js +375 -78
  2. package/package.json +29 -29
  3. package/templates/.qoder/.runtime/ctx-cache-5660152f1d6dd819.md +23 -0
  4. package/templates/.qoder/.runtime/ctx-cache-afdce0dac06b25b0.md +23 -0
  5. package/templates/.qoder/.runtime/search-cache-eae7644e7b122f35.txt +1 -0
  6. package/templates/.qoder/learning/eval-history.jsonl +28 -0
  7. package/templates/data/index/wiki-index.json +8 -0
  8. package/templates/qoder/agents/insight-planning.md +1 -1
  9. package/templates/qoder/agents/insight-research.md +28 -15
  10. package/templates/qoder/commands/optional/wl-insight.md +159 -161
  11. package/templates/qoder/commands/optional/wl-report.md +4 -4
  12. package/templates/qoder/commands/optional/wl-status.md +2 -2
  13. package/templates/qoder/commands/wl-code.md +2 -2
  14. package/templates/qoder/commands/wl-design.md +2 -2
  15. package/templates/qoder/commands/wl-init.md +3 -3
  16. package/templates/qoder/commands/wl-prd.md +2 -2
  17. package/templates/qoder/commands/wl-req.md +43 -0
  18. package/templates/qoder/commands/wl-search.md +8 -8
  19. package/templates/qoder/commands/wl-task.md +17 -17
  20. package/templates/qoder/commands/wl-test.md +41 -15
  21. package/templates/qoder/config.yaml +17 -1
  22. package/templates/qoder/hooks/session-start.py +12 -22
  23. package/templates/qoder/nul +4 -0
  24. package/templates/qoder/rules/wl-pipeline.md +22 -48
  25. package/templates/qoder/scripts/README.md +139 -0
  26. package/templates/qoder/scripts/common/autotest_auth.py +109 -0
  27. package/templates/qoder/scripts/common/bootstrap.py +145 -0
  28. package/templates/qoder/scripts/common/check_publish.py +98 -0
  29. package/templates/qoder/scripts/common/cmd_registry.py +112 -0
  30. package/templates/qoder/scripts/common/config.py +187 -0
  31. package/templates/qoder/scripts/common/contract.py +317 -0
  32. package/templates/qoder/scripts/common/developer.py +2 -1
  33. package/templates/qoder/scripts/common/feishu.py +10 -9
  34. package/templates/qoder/scripts/common/guard.py +159 -0
  35. package/templates/qoder/scripts/common/identity.py +121 -2
  36. package/templates/qoder/scripts/common/kg_capabilities.py +182 -0
  37. package/templates/qoder/scripts/common/mcp_base.py +268 -0
  38. package/templates/qoder/scripts/common/paths.py +187 -1
  39. package/templates/qoder/scripts/common/result.py +223 -0
  40. package/templates/qoder/scripts/common/roles.py +60 -0
  41. package/templates/qoder/scripts/common/task_utils.py +21 -9
  42. package/templates/qoder/scripts/common/test_extract.py +115 -0
  43. package/templates/qoder/scripts/kg/__init__.py +11 -0
  44. package/templates/qoder/scripts/kg/build_entity_registry.py +196 -0
  45. package/templates/qoder/scripts/kg/build_relations.py +127 -0
  46. package/templates/qoder/scripts/{build_style_index.py → kg/build_style_index.py} +39 -10
  47. package/templates/qoder/scripts/kg/build_workflows.py +144 -0
  48. package/templates/qoder/scripts/{context_pack.py → kg/context_pack.py} +18 -11
  49. package/templates/qoder/scripts/{enrich_prompt.py → kg/enrich_prompt.py} +232 -226
  50. package/templates/qoder/scripts/{extract_api_params.py → kg/extract.py} +398 -246
  51. package/templates/qoder/scripts/{kg.py → kg/kg.py} +638 -708
  52. package/templates/qoder/scripts/{kg_build.py → kg/kg_build.py} +618 -612
  53. package/templates/qoder/scripts/{kg_build_db.py → kg/kg_build_db.py} +333 -327
  54. package/templates/qoder/scripts/{kg_duckdb.py → kg/kg_duckdb.py} +38 -37
  55. package/templates/qoder/scripts/{kg_incremental.py → kg/kg_incremental.py} +420 -393
  56. package/templates/qoder/scripts/{kg_link_db.py → kg/kg_link_db.py} +230 -224
  57. package/templates/qoder/scripts/{kg_semantic.py → kg/kg_semantic.py} +156 -150
  58. package/templates/qoder/scripts/kg/prefetch.py +359 -0
  59. package/templates/qoder/scripts/{search_index.py → kg/search_index.py} +70 -14
  60. package/templates/qoder/scripts/mcp/__init__.py +11 -0
  61. package/templates/qoder/scripts/{kg_mcp_server.py → mcp/kg_mcp_server.py} +77 -272
  62. package/templates/qoder/scripts/{lanhu_stdio_wrapper.py → mcp/lanhu_stdio_wrapper.py} +125 -119
  63. package/templates/qoder/scripts/{check_mcp.py → mcp/mcp_doctor.py} +515 -298
  64. package/templates/qoder/scripts/{mcp_launcher.py → mcp/mcp_launcher.py} +442 -414
  65. package/templates/qoder/scripts/{mysql_mcp_server.py → mcp/mysql_mcp_server.py} +347 -396
  66. package/templates/qoder/scripts/{zentao_mcp_server.py → mcp/zentao_mcp_server.py} +384 -424
  67. package/templates/qoder/scripts/report/__init__.py +11 -0
  68. package/templates/qoder/scripts/{add_session.py → report/add_session.py} +250 -244
  69. package/templates/qoder/scripts/{archive_prd.py → report/archive_prd.py} +383 -377
  70. package/templates/qoder/scripts/{eval_prd.py → report/eval_prd.py} +73 -11
  71. package/templates/qoder/scripts/{export.py → report/export.py} +63 -0
  72. package/templates/qoder/scripts/{fill_prototype.py → report/fill_prototype.py} +6 -0
  73. package/templates/qoder/scripts/{gen_design_doc.py → report/gen_design_doc.py} +400 -394
  74. package/templates/qoder/scripts/{learn.py → report/learn.py} +152 -146
  75. package/templates/qoder/scripts/{learn_aggregate.py → report/learn_aggregate.py} +207 -201
  76. package/templates/qoder/scripts/{report.py → report/report.py} +287 -281
  77. package/templates/qoder/scripts/report/req.py +222 -0
  78. package/templates/qoder/scripts/report/role.py +33 -0
  79. package/templates/qoder/scripts/{status.py → report/status.py} +634 -628
  80. package/templates/qoder/scripts/setup/__init__.py +11 -0
  81. package/templates/qoder/scripts/setup/carriers.py +662 -0
  82. package/templates/qoder/scripts/{init_doctor.py → setup/init_doctor.py} +63 -26
  83. package/templates/qoder/scripts/{install_qoderwork.py → setup/install_qoderwork.py} +36 -26
  84. package/templates/qoder/scripts/{platform_doctor.py → setup/platform_doctor.py} +265 -259
  85. package/templates/qoder/scripts/{repo_root.py → setup/repo_root.py} +112 -106
  86. package/templates/qoder/scripts/{setup.py → setup/setup.py} +147 -0
  87. package/templates/qoder/scripts/{setup_lanhu.py → setup/setup_lanhu.py} +973 -963
  88. package/templates/qoder/scripts/task/__init__.py +11 -0
  89. package/templates/qoder/scripts/{git_sync.py → task/git_sync.py} +52 -31
  90. package/templates/qoder/scripts/{syncgate.py → task/syncgate.py} +6 -0
  91. package/templates/qoder/scripts/task/task.py +221 -0
  92. package/templates/qoder/scripts/task/task_lifecycle.py +596 -0
  93. package/templates/qoder/scripts/task/task_query.py +161 -0
  94. package/templates/qoder/scripts/task/task_relations.py +424 -0
  95. package/templates/qoder/scripts/{team_sync.py → task/team_sync.py} +93 -20
  96. package/templates/qoder/scripts/test/__init__.py +11 -0
  97. package/templates/qoder/scripts/{autotest.py → test/autotest.py} +1174 -1751
  98. package/templates/qoder/scripts/{autotest_batch.py → test/autotest_batch.py} +242 -224
  99. package/templates/qoder/scripts/test/autotest_data.py +675 -0
  100. package/templates/qoder/scripts/{autotest_run.py → test/autotest_run.py} +309 -297
  101. package/templates/qoder/scripts/{benchmark.py → test/benchmark.py} +6 -0
  102. package/templates/qoder/scripts/{kg_auto_login.py → test/kg_auto_login.py} +202 -196
  103. package/templates/qoder/scripts/{kg_test_runner.py → test/kg_test_runner.py} +7 -1
  104. package/templates/qoder/scripts/{page_probe.py → test/page_probe.py} +465 -459
  105. package/templates/qoder/scripts/wlkj.py +116 -0
  106. package/templates/qoder/settings.json +1 -10
  107. package/templates/qoder/skills/design-import/SKILL.md +226 -226
  108. package/templates/qoder/skills/design-review/SKILL.md +82 -82
  109. package/templates/qoder/skills/prd-generator/SKILL.md +26 -16
  110. package/templates/qoder/skills/prd-review/SKILL.md +5 -5
  111. package/templates/qoder/skills/prototype-generator/SKILL.md +256 -256
  112. package/templates/qoder/skills/spec-coder/SKILL.md +4 -4
  113. package/templates/qoder/skills/spec-generator/SKILL.md +4 -4
  114. package/templates/qoder/skills/test-generator/SKILL.md +5 -5
  115. package/templates/qoder/skills/wl-code/SKILL.md +4 -4
  116. package/templates/qoder/skills/wl-commit/SKILL.md +4 -4
  117. package/templates/qoder/skills/wl-design/SKILL.md +3 -3
  118. package/templates/qoder/skills/wl-init/SKILL.md +8 -8
  119. package/templates/qoder/skills/wl-insight/SKILL.md +5 -5
  120. package/templates/qoder/skills/wl-prd-full/SKILL.md +6 -6
  121. package/templates/qoder/skills/wl-prd-quick/SKILL.md +6 -6
  122. package/templates/qoder/skills/wl-prd-review/SKILL.md +4 -4
  123. package/templates/qoder/skills/wl-report/SKILL.md +7 -7
  124. package/templates/qoder/skills/wl-search/SKILL.md +13 -13
  125. package/templates/qoder/skills/wl-spec/SKILL.md +5 -5
  126. package/templates/qoder/skills/wl-status/SKILL.md +5 -5
  127. package/templates/qoder/skills/wl-task/SKILL.md +6 -6
  128. package/templates/qoder/skills/wl-test/SKILL.md +102 -39
  129. package/templates/root/AGENTS.md +39 -40
  130. package/templates/qoder/hooks/inject-workflow-state.py +0 -169
  131. package/templates/qoder/scripts/__pycache__/check_mcp_launch.cpython-39.pyc +0 -0
  132. package/templates/qoder/scripts/__pycache__/install_qoderwork.cpython-39.pyc +0 -0
  133. package/templates/qoder/scripts/__pycache__/mcp_launcher.cpython-39.pyc +0 -0
  134. package/templates/qoder/scripts/__pycache__/platform_doctor.cpython-39.pyc +0 -0
  135. package/templates/qoder/scripts/check_carriers.py +0 -238
  136. package/templates/qoder/scripts/check_mcp_launch.py +0 -183
  137. package/templates/qoder/scripts/check_qoderwork_consistency.py +0 -166
  138. package/templates/qoder/scripts/collect_prds.py +0 -31
  139. package/templates/qoder/scripts/common/mentions.py +0 -134
  140. package/templates/qoder/scripts/common/utf8.py +0 -38
  141. package/templates/qoder/scripts/extract_routes.py +0 -54
  142. package/templates/qoder/scripts/extract_routes_tree.py +0 -78
  143. package/templates/qoder/scripts/handoff.py +0 -22
  144. package/templates/qoder/scripts/init_developer.py +0 -76
  145. package/templates/qoder/scripts/parse_prds.py +0 -33
  146. package/templates/qoder/scripts/role.py +0 -51
  147. package/templates/qoder/scripts/sync_carriers.py +0 -259
  148. package/templates/qoder/scripts/task.py +0 -1261
  149. package/templates/qoder/scripts/workspace_init.py +0 -102
  150. package/templates/qoder/skills/prompt-enrich/SKILL.md +0 -90
  151. package/templates/qoder/skills/prototype-generator/SKILL.md.zcode-79180-2af4721f-f9a6-412c-88db-c0af680d211b.tmp +0 -0
  152. /package/templates/qoder/scripts/{secure-ls.js → test/secure-ls.js} +0 -0
@@ -1,246 +1,398 @@
1
- #!/usr/bin/env python3
2
- # -*- coding: utf-8 -*-
3
- """
4
- extract_api_params.py - 从 TS 类型 + Java Bo 提取 API 参数, 生成智能默认值
5
- 读主库 kg.duckdb(只读), api-params.json(避免 DuckDB 写入性能问题)。
6
- 后续 MySQL 接入后, 用真实数据覆盖默认值。
7
- """
8
- import sys, os, re, json, time, glob, duckdb
9
- from datetime import datetime
10
-
11
- try:
12
- sys.stdout.reconfigure(encoding='utf-8', errors='replace')
13
- sys.stderr.reconfigure(encoding='utf-8', errors='replace')
14
- except Exception:
15
- pass
16
-
17
- sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
18
- BASE = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
19
- KG_PATH = os.path.join(BASE, 'data', 'index', 'kg.duckdb')
20
- PARAMS_FILE = os.path.join(BASE, 'data', 'index', 'api-params.json')
21
-
22
-
23
- # ── TS 解析 ──────────────────────────────────────────────────
24
-
25
- def parse_ts_param_types():
26
- """扫所有 TS api 文件, 建 {fn_name: param_type} 映射。"""
27
- fn_map = {}
28
- ts_root = os.path.join(BASE, 'data', 'code', 'fywl-ui', 'apps', 'ics', 'src', 'api')
29
- for tf in glob.glob(os.path.join(ts_root, '**', '*.ts'), recursive=True):
30
- try:
31
- if os.path.getsize(tf) > 50000:
32
- continue
33
- txt = open(tf, encoding='utf-8', errors='ignore').read()
34
- except Exception:
35
- continue
36
- for m in re.finditer(r'export\s+(?:async\s+)?function\s+(\w+)\s*\(\s*(?:\w+)?\s*(?::\s*(\w+|Partial<[^>]+>))?', txt):
37
- fn_map[m.group(1)] = m.group(2) or 'none'
38
- for m in re.finditer(r'export\s+const\s+(\w+)\s*=\s*\(\s*(?:\w+)?\s*(?::\s*(\w+|Partial<[^>]+>|Record<[^>]+>))?', txt):
39
- fn_map[m.group(1)] = m.group(2) or 'none'
40
- return fn_map
41
-
42
-
43
- # ── Java Bo 解析 ─────────────────────────────────────────────
44
-
45
- def build_bo_index():
46
- """预扫所有 Bo/Dto/Query 类, 建 {类名: 路径} 索引。"""
47
- index = {}
48
- root = os.path.join(BASE, 'data', 'code', 'fywl-ics')
49
- for pattern in ['**/*Bo.java', '**/*Dto.java', '**/*QueryBo.java', '**/*Query.java']:
50
- for f in glob.glob(os.path.join(root, pattern), recursive=True):
51
- if 'target' in f or '/classes/' in f:
52
- continue
53
- base_name = os.path.basename(f).replace('.java', '')
54
- for suffix in ['QueryBo', 'Bo', 'Dto', 'Query']:
55
- if base_name.endswith(suffix):
56
- index[base_name] = f
57
- short = base_name[:-len(suffix)]
58
- if short not in index:
59
- index[short] = f
60
- break
61
- else:
62
- index[base_name] = f
63
- return index
64
-
65
-
66
- def gen_default(java_type, field_name):
67
- """按 Java 类型 + 字段名生成智能默认值。"""
68
- FIELD_DEFAULTS = {
69
- 'pageNum': 1, 'pageSize': 10, 'page': 1, 'size': 10, 'current': 1,
70
- 'id': 1, 'parentId': 0, 'pid': 0, 'deptId': 100,
71
- 'userId': 1, 'tenantId': '000000',
72
- 'status': 0, 'type': '', 'code': '', 'name': 'test',
73
- 'phone': '13800000000', 'mobile': '13800000000', 'email': 'test@test.com',
74
- 'isAsc': 'asc', 'orderByColumn': 'id',
75
- }
76
- if field_name in FIELD_DEFAULTS:
77
- return FIELD_DEFAULTS[field_name]
78
- fn = (field_name or '').lower()
79
- if 'phone' in fn or 'mobile' in fn:
80
- return '13800000000'
81
- if 'email' in fn:
82
- return 'test@test.com'
83
- if 'date' in fn or 'time' in fn:
84
- return '2026-01-01'
85
- if 'name' in fn:
86
- return 'test'
87
- if 'code' in fn:
88
- return 'TEST001'
89
- if fn == 'id' or fn.endswith('id'):
90
- return 1
91
- jt = (java_type or '').strip()
92
- if jt in ('Long', 'long', 'Integer', 'int', 'BigInteger', 'BigDecimal', 'Number'):
93
- return 1
94
- if jt in ('String', 'CharSequence', 'char'):
95
- return 'test'
96
- if jt in ('Boolean', 'boolean'):
97
- return False
98
- if jt in ('Date', 'LocalDate', 'LocalDateTime', 'Timestamp'):
99
- return '2026-01-01'
100
- if jt in ('Double', 'double', 'Float', 'float'):
101
- return 1.0
102
- if jt.startswith('List') or jt.startswith('Set') or jt.startswith('Collection'):
103
- return []
104
- if jt.startswith('Map'):
105
- return {}
106
- return None
107
-
108
-
109
- def parse_bo_fields(java_path):
110
- """解析 Java Bo 类的字段+类型+校验注解。"""
111
- try:
112
- if not os.path.isfile(java_path) or os.path.getsize(java_path) > 80000:
113
- return []
114
- txt = open(java_path, encoding='utf-8', errors='ignore').read()
115
- except Exception:
116
- return []
117
- fields = []
118
- try:
119
- for m in re.finditer(
120
- r'((?:@\w+(?:\([^)]*\))?\s*)*)'
121
- r'(?:private\s+)?(\w+(?:<[^>]+>)?)\s+(\w+)\s*;', txt):
122
- anns, jtype, fname = m.group(1), m.group(2), m.group(3)
123
- if fname in ('serialVersionUID', 'logger', 'LOG') or fname.isupper():
124
- continue
125
- required = bool(re.search(r'@(?:NotNull|NotBlank|NotEmpty|Valid)\b', anns))
126
- fields.append({'name': fname, 'type': jtype, 'required': required})
127
- except Exception:
128
- pass
129
- return fields
130
-
131
-
132
- _BO_CACHE = {}
133
-
134
-
135
- def bo_to_params(bo_class, bo_index):
136
- """从 Bo 类名解析出参数 JSON (带缓存)。"""
137
- if not bo_class or bo_class in _BO_CACHE:
138
- return _BO_CACHE.get(bo_class)
139
- jf = bo_index.get(bo_class) or bo_index.get(bo_class + 'Bo') or bo_index.get(bo_class + 'Dto')
140
- if not jf:
141
- _BO_CACHE[bo_class] = None
142
- return None
143
- fields = parse_bo_fields(jf)
144
- if not fields:
145
- _BO_CACHE[bo_class] = None
146
- return None
147
- params = {}
148
- for f in fields:
149
- val = gen_default(f['type'], f['name'])
150
- if f['required'] and val is None:
151
- val = 1 if 'id' in f['name'].lower() else 'test'
152
- params[f['name']] = val
153
- _BO_CACHE[bo_class] = params
154
- return params
155
-
156
-
157
- # ── 参数生成 ─────────────────────────────────────────────────
158
-
159
- def gen_params(ptype, bo_index=None):
160
- """按 TS 参数类型生成默认参数 JSON。Bo 类型解析 Java 字段。"""
161
- if ptype == 'PageQuery':
162
- return {'pageNum': 1, 'pageSize': 10, 'isAsc': 'asc', 'orderByColumn': 'id'}, 'PageQuery'
163
- if ptype in ('ID', 'Id'):
164
- return {'id': 1}, 'ID'
165
- if ptype == 'IDS':
166
- return {'ids': [1]}, 'IDS'
167
- if ptype == 'string':
168
- return {'id': '1'}, 'string'
169
- if ptype == 'number':
170
- return {'id': 1}, 'number'
171
- if ptype and ptype not in ('any', 'none', 'Recordable', 'Partial', 'Record', 'boolean', 'Blob'):
172
- if bo_index:
173
- bp = bo_to_params(ptype, bo_index)
174
- if bp:
175
- return bp, 'Bo:' + ptype
176
- return {}, 'Bo:' + ptype
177
- if ptype == 'any':
178
- return {}, 'any'
179
- return {}, 'none'
180
-
181
-
182
- # ── 主流程 ───────────────────────────────────────────────────
183
-
184
- def main():
185
- t0 = time.time()
186
-
187
- # 1. 读主库(只读)拿接口列表
188
- kg = duckdb.connect(KG_PATH, read_only=True)
189
- rows = kg.execute("SELECT fn_name, url, verb FROM api_functions WHERE url IS NOT NULL").fetchall()
190
- kg.close()
191
- sys.stderr.write('[%.1fs] DuckDB: %d rows\n' % (time.time() - t0, len(rows)))
192
-
193
- # 2. TS 参数类型
194
- fn_map = parse_ts_param_types()
195
- sys.stderr.write('[%.1fs] TS: %d fns\n' % (time.time() - t0, len(fn_map)))
196
-
197
- # 2b. Bo 类索引
198
- bo_index = build_bo_index()
199
- sys.stderr.write('[%.1fs] Bo index: %d\n' % (time.time() - t0, len(bo_index)))
200
-
201
- # 3. 生成参数
202
- results = []
203
- source_dist = {}
204
- for fn, url, verb in rows:
205
- if not url or url.startswith('$'):
206
- continue
207
- ptype = fn_map.get(fn, 'none')
208
- params, source = gen_params(ptype, bo_index)
209
- req = [k for k, v in params.items() if v is not None]
210
- results.append({
211
- 'url': url, 'verb': verb or '', 'param_source': source,
212
- 'params': params, 'required': req,
213
- })
214
- skey = source.split(':')[0]
215
- source_dist[skey] = source_dist.get(skey, 0) + 1
216
-
217
- sys.stderr.write('[%.1fs] Generated: %d\n' % (time.time() - t0, len(results)))
218
-
219
- # 4. 写 JSON
220
- output = {
221
- 'version': '1.0',
222
- 'extracted_at': datetime.now().isoformat()[:19],
223
- 'total': len(results),
224
- 'apis': results,
225
- }
226
- with open(PARAMS_FILE, 'w', encoding='utf-8') as f:
227
- json.dump(output, f, ensure_ascii=False, indent=2)
228
- sys.stderr.write('[%.1fs] Written: %s\n' % (time.time() - t0, PARAMS_FILE))
229
-
230
- # 5. 报告
231
- sys.stderr.write('\n=== API 参数提取 ===\n')
232
- sys.stderr.write('总接口: %d, 已提取: %d\n' % (len(rows), len(results)))
233
- for s, c in sorted(source_dist.items(), key=lambda x: -x[1]):
234
- sys.stderr.write(' %-15s %d\n' % (s, c))
235
-
236
- # 样本
237
- for r in results:
238
- if r['param_source'] not in ('none', 'any') and r['params']:
239
- sys.stderr.write('\n样本: %s [%s] %s\n' % (
240
- r['url'][:50], r['param_source'],
241
- json.dumps(r['params'], ensure_ascii=False)[:60]))
242
- break
243
-
244
-
245
- if __name__ == '__main__':
246
- main()
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """extract.py - 三种提取策略 (Strategy 模式, v3.0 合并3文件)
4
+
5
+ v3.0 合并自: extract_routes(regex扫路由) + extract_routes_tree(API拉路由树) + extract_api_params(提取API参数)。
6
+ 三种"提取"是同一抽象的不同策略, 合一用子命令分发。
7
+
8
+ 用法:
9
+ python extract.py routes regex扫前端路由→frontend-routes.json
10
+ python extract.py routes-tree API拉菜单树→menu-routes-tree.json (需登录态)
11
+ python extract.py api-params 提取API参数→api-params.json
12
+ """
13
+ # v3.0 路径自举: 引导到 common/bootstrap, 统一 sys.path 逻辑
14
+ import os as _o, sys as _s
15
+ _cp = _o.path.join(_o.path.dirname(_o.path.dirname(_o.path.abspath(__file__))), 'common')
16
+ if _cp not in _s.path: _s.path.insert(0, _cp)
17
+ from bootstrap import setup; setup()
18
+
19
+ import sys, os, re, glob, json
20
+ from common.paths import PROJECT_ROOT
21
+
22
+ # ==================== routes-tree 的辅助函数 ====================
23
+ def read_developer():
24
+ f = os.path.join(PROJECT, '.qoder', '.developer')
25
+ for ln in open(f, encoding='utf-8'):
26
+ if ln.strip().startswith('name='):
27
+ return ln.strip().split('=', 1)[1].strip()
28
+ raise RuntimeError('无 .developer, /wl-init')
29
+
30
+
31
+
32
+ # ==================== Strategy 1: routes (regex扫) ====================
33
+ def main_routes():
34
+ """提取前端路由 (regex扫.ts文件). 原extract_routes.py"""
35
+ router_dir = str(PROJECT_ROOT / 'data' / 'code' / 'fywl-ui' / 'apps' / 'ics' / 'src' / 'router')
36
+ routes = []
37
+
38
+ for rf in glob.glob(os.path.join(router_dir, '**', '*.ts'), recursive=True):
39
+ try:
40
+ txt = open(rf, encoding='utf-8', errors='ignore').read()
41
+ except Exception:
42
+ continue
43
+ # 匹配 path: 'xxx' component: () => import('xxx')
44
+ # 或 path: 'xxx', name: 'yyy'
45
+ for m in re.finditer(
46
+ r"path:\s*['\"`]([^'\"`]+)['\"`]"
47
+ r"[^}]*?(?:name:\s*['\"]([^'\"]+)['\"])?"
48
+ r"[^}]*?(?:component:\s*\([^)]*\)\s*=>\s*import\(['\"]([^'\"]+)['\"]\))?",
49
+ txt, re.S):
50
+ path, name, component = m.group(1), m.group(2) or '', m.group(3) or ''
51
+ if path and not path.startswith(':') and len(path) > 1:
52
+ routes.append({'path': path, 'name': name, 'component': component})
53
+
54
+ # 也扫后台菜单路由(通常在数据库 sys_menu 表, 但前端可能有硬编码)
55
+ # 搜 views 目录下的 Vue 文件路径作为页面标识
56
+ views_dir = str(PROJECT_ROOT / 'data' / 'code' / 'fywl-ui' / 'apps' / 'ics' / 'src' / 'views')
57
+ vue_views = glob.glob(os.path.join(views_dir, '**', '*.vue'), recursive=True)
58
+ view_paths = [v.replace('\\', '/').split('/views/')[-1].replace('.vue', '') for v in vue_views]
59
+
60
+ print('路由定义: %d 条' % len(routes))
61
+ print('Vue 页面: %d 个' % len(view_paths))
62
+
63
+ # 保存路由索引
64
+ output = {
65
+ 'version': '1.0',
66
+ 'total_routes': len(routes),
67
+ 'total_views': len(view_paths),
68
+ 'routes': routes,
69
+ 'views': sorted(view_paths),
70
+ }
71
+ out_path = str(PROJECT_ROOT / 'data' / 'index' / 'frontend-routes.json')
72
+ with open(out_path, 'w', encoding='utf-8') as f:
73
+ json.dump(output, f, ensure_ascii=False, indent=2)
74
+ print('已保存: %s' % out_path)
75
+
76
+ # 样本
77
+ print('\n样本路由:')
78
+ for r in routes[:8]:
79
+ print(' %s -> %s' % (r['path'], r['component'][:50] if r['component'] else r['name']))
80
+
81
+ # 保险相关
82
+ print('\n保险相关路由:')
83
+ for r in routes:
84
+ if 'insur' in r['path'].lower() or '保险' in r.get('name', ''):
85
+ print(' %s' % r['path'])
86
+
87
+
88
+ # ==================== Strategy 2: routes-tree (API拉) ====================
89
+ def main_routes_tree():
90
+ """从getRouters API拉菜单树(准确path). 原extract_routes_tree.py"""
91
+ dev = read_developer()
92
+ auth_file = os.path.join(PROJECT, 'workspace', 'members', dev, 'auth-state.json')
93
+ if not os.path.exists(auth_file):
94
+ print(' auth-state, 先登录: python .qoder/scripts/kg_auto_login.py'); sys.exit(1)
95
+ auth = json.load(open(auth_file, encoding='utf-8'))
96
+ token = auth['access_token']
97
+ base = auth['base_url']; prefix = auth.get('api_prefix', '/test-api')
98
+
99
+ print('拉取 getRouters...')
100
+ r = requests.get(base + prefix + '/system/menu/getRouters',
101
+ headers={'Authorization': 'Bearer ' + token, 'clientid': 'e5cd7e4891bf95d1d19206ce24a7b32e'},
102
+ timeout=20)
103
+ d = r.json()
104
+ if d.get('code') != 200:
105
+ print('getRouters 失败:', d.get('msg')); sys.exit(1)
106
+ tree = d.get('data') or []
107
+
108
+
109
+ def walk(nodes, parent_path=''):
110
+ """递归拼接准确 path, 返回 [{full_path, name, component}].
111
+ name 优先用 meta.title(中文), 回退 name(i18n key)."""
112
+ out = []
113
+ for n in nodes:
114
+ seg = n.get('path', '').lstrip('/')
115
+ meta = n.get('meta') or {}
116
+ name = meta.get('title', '') or n.get('name', '')
117
+ comp = n.get('component', '')
118
+ full = (parent_path.rstrip('/') + '/' + seg) if parent_path else '/' + seg
119
+ while '//' in full:
120
+ full = full.replace('//', '/')
121
+ kids = n.get('children') or []
122
+ if kids:
123
+ out.extend(walk(kids, full))
124
+ elif comp and comp != '#' and ':' not in full:
125
+ out.append({'path': full, 'name': name, 'component': comp, 'i18n_key': n.get('name', '')})
126
+ return out
127
+
128
+
129
+ pages = walk(tree)
130
+ # 去重
131
+ seen = set(); uniq = []
132
+ for p in pages:
133
+ if p['path'] not in seen:
134
+ seen.add(p['path']); uniq.append(p)
135
+
136
+ out_file = os.path.join(PROJECT, 'data', 'index', 'menu-routes-tree.json')
137
+ json.dump({
138
+ 'source': 'getRouters API (父子树递归拼接, 准确 path)',
139
+ 'total': len(uniq),
140
+ 'routes': uniq,
141
+ }, open(out_file, 'w', encoding='utf-8'), ensure_ascii=False, indent=2)
142
+
143
+ print('已存 %d 条准确路由 -> %s' % (len(uniq), out_file))
144
+ print()
145
+ # 验证关键页面
146
+ for kw in ['abnormalRecords', 'insurance', 'ticket']:
147
+ hits = [p for p in uniq if kw.lower() in (p['path'] + p['name']).lower()]
148
+ for h in hits[:2]:
149
+ print(' %-12s -> %s' % (h['name'][:10], h['path']))
150
+
151
+
152
+ # ==================== Strategy 3: api-params (来自extract_api_params) ====================
153
+ BASE = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
154
+ KG_PATH = os.path.join(BASE, 'data', 'index', 'kg.duckdb')
155
+ PARAMS_FILE = os.path.join(BASE, 'data', 'index', 'api-params.json')
156
+
157
+
158
+ # ── TS 解析 ──────────────────────────────────────────────────
159
+
160
+ def parse_ts_param_types():
161
+ """扫所有 TS api 文件, 建 {fn_name: param_type} 映射。"""
162
+ fn_map = {}
163
+ ts_root = os.path.join(BASE, 'data', 'code', 'fywl-ui', 'apps', 'ics', 'src', 'api')
164
+ for tf in glob.glob(os.path.join(ts_root, '**', '*.ts'), recursive=True):
165
+ try:
166
+ if os.path.getsize(tf) > 50000:
167
+ continue
168
+ txt = open(tf, encoding='utf-8', errors='ignore').read()
169
+ except Exception:
170
+ continue
171
+ for m in re.finditer(r'export\s+(?:async\s+)?function\s+(\w+)\s*\(\s*(?:\w+)?\s*(?::\s*(\w+|Partial<[^>]+>))?', txt):
172
+ fn_map[m.group(1)] = m.group(2) or 'none'
173
+ for m in re.finditer(r'export\s+const\s+(\w+)\s*=\s*\(\s*(?:\w+)?\s*(?::\s*(\w+|Partial<[^>]+>|Record<[^>]+>))?', txt):
174
+ fn_map[m.group(1)] = m.group(2) or 'none'
175
+ return fn_map
176
+
177
+
178
+ # ── Java Bo 解析 ─────────────────────────────────────────────
179
+
180
+ def build_bo_index():
181
+ """预扫所有 Bo/Dto/Query 类, 建 {类名: 路径} 索引。"""
182
+ index = {}
183
+ root = os.path.join(BASE, 'data', 'code', 'fywl-ics')
184
+ for pattern in ['**/*Bo.java', '**/*Dto.java', '**/*QueryBo.java', '**/*Query.java']:
185
+ for f in glob.glob(os.path.join(root, pattern), recursive=True):
186
+ if 'target' in f or '/classes/' in f:
187
+ continue
188
+ base_name = os.path.basename(f).replace('.java', '')
189
+ for suffix in ['QueryBo', 'Bo', 'Dto', 'Query']:
190
+ if base_name.endswith(suffix):
191
+ index[base_name] = f
192
+ short = base_name[:-len(suffix)]
193
+ if short not in index:
194
+ index[short] = f
195
+ break
196
+ else:
197
+ index[base_name] = f
198
+ return index
199
+
200
+
201
+ def gen_default(java_type, field_name):
202
+ """按 Java 类型 + 字段名生成智能默认值。"""
203
+ FIELD_DEFAULTS = {
204
+ 'pageNum': 1, 'pageSize': 10, 'page': 1, 'size': 10, 'current': 1,
205
+ 'id': 1, 'parentId': 0, 'pid': 0, 'deptId': 100,
206
+ 'userId': 1, 'tenantId': '000000',
207
+ 'status': 0, 'type': '', 'code': '', 'name': 'test',
208
+ 'phone': '13800000000', 'mobile': '13800000000', 'email': 'test@test.com',
209
+ 'isAsc': 'asc', 'orderByColumn': 'id',
210
+ }
211
+ if field_name in FIELD_DEFAULTS:
212
+ return FIELD_DEFAULTS[field_name]
213
+ fn = (field_name or '').lower()
214
+ if 'phone' in fn or 'mobile' in fn:
215
+ return '13800000000'
216
+ if 'email' in fn:
217
+ return 'test@test.com'
218
+ if 'date' in fn or 'time' in fn:
219
+ return '2026-01-01'
220
+ if 'name' in fn:
221
+ return 'test'
222
+ if 'code' in fn:
223
+ return 'TEST001'
224
+ if fn == 'id' or fn.endswith('id'):
225
+ return 1
226
+ jt = (java_type or '').strip()
227
+ if jt in ('Long', 'long', 'Integer', 'int', 'BigInteger', 'BigDecimal', 'Number'):
228
+ return 1
229
+ if jt in ('String', 'CharSequence', 'char'):
230
+ return 'test'
231
+ if jt in ('Boolean', 'boolean'):
232
+ return False
233
+ if jt in ('Date', 'LocalDate', 'LocalDateTime', 'Timestamp'):
234
+ return '2026-01-01'
235
+ if jt in ('Double', 'double', 'Float', 'float'):
236
+ return 1.0
237
+ if jt.startswith('List') or jt.startswith('Set') or jt.startswith('Collection'):
238
+ return []
239
+ if jt.startswith('Map'):
240
+ return {}
241
+ return None
242
+
243
+
244
+ def parse_bo_fields(java_path):
245
+ """解析 Java Bo 类的字段+类型+校验注解。"""
246
+ try:
247
+ if not os.path.isfile(java_path) or os.path.getsize(java_path) > 80000:
248
+ return []
249
+ txt = open(java_path, encoding='utf-8', errors='ignore').read()
250
+ except Exception:
251
+ return []
252
+ fields = []
253
+ try:
254
+ for m in re.finditer(
255
+ r'((?:@\w+(?:\([^)]*\))?\s*)*)'
256
+ r'(?:private\s+)?(\w+(?:<[^>]+>)?)\s+(\w+)\s*;', txt):
257
+ anns, jtype, fname = m.group(1), m.group(2), m.group(3)
258
+ if fname in ('serialVersionUID', 'logger', 'LOG') or fname.isupper():
259
+ continue
260
+ required = bool(re.search(r'@(?:NotNull|NotBlank|NotEmpty|Valid)\b', anns))
261
+ fields.append({'name': fname, 'type': jtype, 'required': required})
262
+ except Exception:
263
+ pass
264
+ return fields
265
+
266
+
267
+ _BO_CACHE = {}
268
+
269
+
270
+ def bo_to_params(bo_class, bo_index):
271
+ """从 Bo 类名解析出参数 JSON (带缓存)。"""
272
+ if not bo_class or bo_class in _BO_CACHE:
273
+ return _BO_CACHE.get(bo_class)
274
+ jf = bo_index.get(bo_class) or bo_index.get(bo_class + 'Bo') or bo_index.get(bo_class + 'Dto')
275
+ if not jf:
276
+ _BO_CACHE[bo_class] = None
277
+ return None
278
+ fields = parse_bo_fields(jf)
279
+ if not fields:
280
+ _BO_CACHE[bo_class] = None
281
+ return None
282
+ params = {}
283
+ for f in fields:
284
+ val = gen_default(f['type'], f['name'])
285
+ if f['required'] and val is None:
286
+ val = 1 if 'id' in f['name'].lower() else 'test'
287
+ params[f['name']] = val
288
+ _BO_CACHE[bo_class] = params
289
+ return params
290
+
291
+
292
+ # ── 参数生成 ─────────────────────────────────────────────────
293
+
294
+ def gen_params(ptype, bo_index=None):
295
+ """按 TS 参数类型生成默认参数 JSON。Bo 类型解析 Java 字段。"""
296
+ if ptype == 'PageQuery':
297
+ return {'pageNum': 1, 'pageSize': 10, 'isAsc': 'asc', 'orderByColumn': 'id'}, 'PageQuery'
298
+ if ptype in ('ID', 'Id'):
299
+ return {'id': 1}, 'ID'
300
+ if ptype == 'IDS':
301
+ return {'ids': [1]}, 'IDS'
302
+ if ptype == 'string':
303
+ return {'id': '1'}, 'string'
304
+ if ptype == 'number':
305
+ return {'id': 1}, 'number'
306
+ if ptype and ptype not in ('any', 'none', 'Recordable', 'Partial', 'Record', 'boolean', 'Blob'):
307
+ if bo_index:
308
+ bp = bo_to_params(ptype, bo_index)
309
+ if bp:
310
+ return bp, 'Bo:' + ptype
311
+ return {}, 'Bo:' + ptype
312
+ if ptype == 'any':
313
+ return {}, 'any'
314
+ return {}, 'none'
315
+
316
+
317
+ # ── 主流程 ───────────────────────────────────────────────────
318
+
319
+ def main_api_params():
320
+ t0 = time.time()
321
+
322
+ # 1. 读主库(只读)拿接口列表
323
+ kg = duckdb.connect(KG_PATH, read_only=True)
324
+ rows = kg.execute("SELECT fn_name, url, verb FROM api_functions WHERE url IS NOT NULL").fetchall()
325
+ kg.close()
326
+ sys.stderr.write('[%.1fs] DuckDB: %d rows\n' % (time.time() - t0, len(rows)))
327
+
328
+ # 2. TS 参数类型
329
+ fn_map = parse_ts_param_types()
330
+ sys.stderr.write('[%.1fs] TS: %d fns\n' % (time.time() - t0, len(fn_map)))
331
+
332
+ # 2b. Bo 类索引
333
+ bo_index = build_bo_index()
334
+ sys.stderr.write('[%.1fs] Bo index: %d\n' % (time.time() - t0, len(bo_index)))
335
+
336
+ # 3. 生成参数
337
+ results = []
338
+ source_dist = {}
339
+ for fn, url, verb in rows:
340
+ if not url or url.startswith('$'):
341
+ continue
342
+ ptype = fn_map.get(fn, 'none')
343
+ params, source = gen_params(ptype, bo_index)
344
+ req = [k for k, v in params.items() if v is not None]
345
+ results.append({
346
+ 'url': url, 'verb': verb or '', 'param_source': source,
347
+ 'params': params, 'required': req,
348
+ })
349
+ skey = source.split(':')[0]
350
+ source_dist[skey] = source_dist.get(skey, 0) + 1
351
+
352
+ sys.stderr.write('[%.1fs] Generated: %d\n' % (time.time() - t0, len(results)))
353
+
354
+ # 4. 写 JSON
355
+ output = {
356
+ 'version': '1.0',
357
+ 'extracted_at': datetime.now().isoformat()[:19],
358
+ 'total': len(results),
359
+ 'apis': results,
360
+ }
361
+ with open(PARAMS_FILE, 'w', encoding='utf-8') as f:
362
+ json.dump(output, f, ensure_ascii=False, indent=2)
363
+ sys.stderr.write('[%.1fs] Written: %s\n' % (time.time() - t0, PARAMS_FILE))
364
+
365
+ # 5. 报告
366
+ sys.stderr.write('\n=== API 参数提取 ===\n')
367
+ sys.stderr.write('总接口: %d, 已提取: %d\n' % (len(rows), len(results)))
368
+ for s, c in sorted(source_dist.items(), key=lambda x: -x[1]):
369
+ sys.stderr.write(' %-15s %d\n' % (s, c))
370
+
371
+ # 样本
372
+ for r in results:
373
+ if r['param_source'] not in ('none', 'any') and r['params']:
374
+ sys.stderr.write('\n样本: %s [%s] %s\n' % (
375
+ r['url'][:50], r['param_source'],
376
+ json.dumps(r['params'], ensure_ascii=False)[:60]))
377
+ break
378
+
379
+
380
+
381
+
382
+ # ============================================================
383
+ # Facade: 统一入口分发
384
+ # ============================================================
385
+ def main():
386
+ cmd = sys.argv[1] if len(sys.argv) > 1 else 'routes'
387
+ sys.argv = [sys.argv[0]] + sys.argv[2:]
388
+ if cmd == 'routes':
389
+ main_routes()
390
+ elif cmd == 'routes-tree':
391
+ main_routes_tree()
392
+ elif cmd == 'api-params':
393
+ main_api_params()
394
+ else:
395
+ print('用法: extract.py [routes|routes-tree|api-params]')
396
+
397
+ if __name__ == '__main__':
398
+ main()