@hupan56/wlkj 2.6.0 → 2.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/bin/cli.js +269 -11
  2. package/package.json +1 -1
  3. package/templates/qoder/agents/insight-planning.md +67 -0
  4. package/templates/qoder/agents/insight-research.md +61 -0
  5. package/templates/qoder/agents/prd-reference.md +8 -2
  6. package/templates/qoder/commands/optional/wl-insight.md +275 -0
  7. package/templates/qoder/commands/{wl-report.md → optional/wl-report.md} +13 -5
  8. package/templates/qoder/commands/{wl-spec.md → optional/wl-spec.md} +1 -1
  9. package/templates/qoder/commands/{wl-status.md → optional/wl-status.md} +28 -2
  10. package/templates/qoder/commands/wl-code.md +10 -2
  11. package/templates/qoder/commands/wl-commit.md +1 -1
  12. package/templates/qoder/commands/wl-design-draw.md +78 -0
  13. package/templates/qoder/commands/wl-design-scan.md +108 -0
  14. package/templates/qoder/commands/wl-design-spec.md +154 -0
  15. package/templates/qoder/commands/wl-design.md +32 -0
  16. package/templates/qoder/commands/wl-init.md +24 -3
  17. package/templates/qoder/commands/wl-prd-full.md +226 -0
  18. package/templates/qoder/commands/wl-prd-quick.md +134 -0
  19. package/templates/qoder/commands/wl-prd-review.md +104 -0
  20. package/templates/qoder/commands/wl-prd.md +17 -311
  21. package/templates/qoder/commands/wl-search.md +66 -30
  22. package/templates/qoder/commands/wl-task.md +290 -59
  23. package/templates/qoder/commands/wl-test.md +92 -24
  24. package/templates/qoder/config.yaml +51 -15
  25. package/templates/qoder/hooks/inject-workflow-state.py +2 -2
  26. package/templates/qoder/hooks/session-start.py +82 -51
  27. package/templates/qoder/rules/wl-pipeline.md +216 -105
  28. package/templates/qoder/scripts/__pycache__/search_index.cpython-39.pyc +0 -0
  29. package/templates/qoder/scripts/archive_prd.py +377 -0
  30. package/templates/qoder/scripts/autotest.py +1715 -0
  31. package/templates/qoder/scripts/autotest_batch.py +224 -0
  32. package/templates/qoder/scripts/autotest_run.py +297 -0
  33. package/templates/qoder/scripts/benchmark.py +210 -209
  34. package/templates/qoder/scripts/build_style_index.py +444 -4
  35. package/templates/qoder/scripts/check_carriers.py +238 -0
  36. package/templates/qoder/scripts/check_mcp.py +298 -0
  37. package/templates/qoder/scripts/check_qoderwork_consistency.py +166 -0
  38. package/templates/qoder/scripts/common/events.py +46 -0
  39. package/templates/qoder/scripts/common/extract.py +419 -0
  40. package/templates/qoder/scripts/common/graph_traverse.py +533 -0
  41. package/templates/qoder/scripts/common/paths.py +89 -0
  42. package/templates/qoder/scripts/common/pip_install.py +144 -0
  43. package/templates/qoder/scripts/common/platform_guard.py +61 -0
  44. package/templates/qoder/scripts/common/search_engine.py +205 -205
  45. package/templates/qoder/scripts/common/terms.py +57 -0
  46. package/templates/qoder/scripts/common/ts_extract.py +536 -0
  47. package/templates/qoder/scripts/context_pack.py +73 -13
  48. package/templates/qoder/scripts/enrich_prompt.py +226 -0
  49. package/templates/qoder/scripts/eval_prd.py +318 -231
  50. package/templates/qoder/scripts/export.py +487 -487
  51. package/templates/qoder/scripts/extract_api_params.py +246 -0
  52. package/templates/qoder/scripts/extract_routes.py +54 -0
  53. package/templates/qoder/scripts/extract_routes_tree.py +78 -0
  54. package/templates/qoder/scripts/fill_prototype.py +707 -0
  55. package/templates/qoder/scripts/gen_design_doc.py +394 -0
  56. package/templates/qoder/scripts/git_sync.py +27 -15
  57. package/templates/qoder/scripts/init_doctor.py +292 -40
  58. package/templates/qoder/scripts/install_qoderwork.py +366 -9
  59. package/templates/qoder/scripts/kg.py +708 -0
  60. package/templates/qoder/scripts/kg_auto_login.py +196 -0
  61. package/templates/qoder/scripts/kg_build.py +612 -0
  62. package/templates/qoder/scripts/kg_build_db.py +327 -0
  63. package/templates/qoder/scripts/kg_duckdb.py +549 -0
  64. package/templates/qoder/scripts/kg_incremental.py +393 -0
  65. package/templates/qoder/scripts/kg_link_db.py +224 -0
  66. package/templates/qoder/scripts/kg_mcp_server.py +801 -0
  67. package/templates/qoder/scripts/kg_semantic.py +150 -0
  68. package/templates/qoder/scripts/kg_test_runner.py +241 -0
  69. package/templates/qoder/scripts/lanhu_stdio_wrapper.py +119 -0
  70. package/templates/qoder/scripts/learn.py +118 -39
  71. package/templates/qoder/scripts/learn_aggregate.py +201 -0
  72. package/templates/qoder/scripts/mcp_launcher.py +359 -0
  73. package/templates/qoder/scripts/mysql_mcp_server.py +396 -0
  74. package/templates/qoder/scripts/repo_root.py +106 -0
  75. package/templates/qoder/scripts/role.py +12 -0
  76. package/templates/qoder/scripts/run_weekly_update.bat +5 -0
  77. package/templates/qoder/scripts/run_weekly_update.sh +5 -0
  78. package/templates/qoder/scripts/search_index.py +307 -60
  79. package/templates/qoder/scripts/secure-ls.js +5640 -0
  80. package/templates/qoder/scripts/setup.py +706 -641
  81. package/templates/qoder/scripts/setup_lanhu.py +963 -0
  82. package/templates/qoder/scripts/status.py +250 -11
  83. package/templates/qoder/scripts/sync_carriers.py +259 -0
  84. package/templates/qoder/scripts/syncgate.py +2 -2
  85. package/templates/qoder/scripts/task.py +75 -0
  86. package/templates/qoder/scripts/team_sync.py +60 -4
  87. package/templates/qoder/scripts/workspace_init.py +1 -1
  88. package/templates/qoder/skills/design-import/SKILL.md +226 -0
  89. package/templates/qoder/skills/design-import/figma-workflow.md +81 -0
  90. package/templates/qoder/skills/design-review/SKILL.md +82 -25
  91. package/templates/qoder/skills/prd-generator/SKILL.md +184 -60
  92. package/templates/qoder/skills/prd-review/SKILL.md +18 -1
  93. package/templates/qoder/skills/prompt-enrich/SKILL.md +90 -0
  94. package/templates/qoder/skills/prototype-generator/SKILL.md +256 -141
  95. package/templates/qoder/skills/prototype-generator/SKILL.md.zcode-79180-2af4721f-f9a6-412c-88db-c0af680d211b.tmp +0 -0
  96. package/templates/qoder/skills/spec-coder/SKILL.md +18 -1
  97. package/templates/qoder/skills/spec-generator/SKILL.md +18 -1
  98. package/templates/qoder/skills/test-generator/SKILL.md +15 -2
  99. package/templates/qoder/skills/wl-code/SKILL.md +55 -36
  100. package/templates/qoder/skills/wl-commit/SKILL.md +89 -76
  101. package/templates/qoder/skills/wl-design/SKILL.md +55 -0
  102. package/templates/qoder/skills/wl-init/SKILL.md +76 -67
  103. package/templates/qoder/skills/wl-insight/SKILL.md +201 -81
  104. package/templates/qoder/skills/wl-prd-full/SKILL.md +69 -0
  105. package/templates/qoder/skills/wl-prd-quick/SKILL.md +49 -0
  106. package/templates/qoder/skills/wl-prd-review/SKILL.md +34 -0
  107. package/templates/qoder/skills/wl-report/SKILL.md +131 -107
  108. package/templates/qoder/skills/wl-search/SKILL.md +141 -75
  109. package/templates/qoder/skills/wl-spec/SKILL.md +49 -39
  110. package/templates/qoder/skills/wl-status/SKILL.md +83 -61
  111. package/templates/qoder/skills/wl-task/SKILL.md +132 -58
  112. package/templates/qoder/skills/wl-test/SKILL.md +406 -40
  113. package/templates/qoder/templates/prototype-app.html +13 -8
  114. package/templates/qoder/templates/prototype-web.html +376 -93
  115. package/templates/root/AGENTS.md +89 -34
  116. package/templates/root/requirements.txt +21 -0
  117. package/templates/root//344/275/277/347/224/250/350/257/264/346/230/216.md +259 -259
  118. package/templates/root//346/226/260/346/211/213/346/214/207/345/215/227.md +186 -186
  119. package/templates/qoder/agents/prd-planning.md +0 -57
  120. package/templates/qoder/agents/prd-research.md +0 -34
  121. package/templates/qoder/commands/wl-insight.md +0 -51
  122. package/templates/qoder/scripts/__pycache__/init_doctor.cpython-39.pyc +0 -0
  123. package/templates/qoder/scripts/__pycache__/setup.cpython-39.pyc +0 -0
  124. package/templates/qoder/scripts/common/__pycache__/developer.cpython-39.pyc +0 -0
  125. package/templates/qoder/skills/wl-prd/SKILL.md +0 -92
@@ -0,0 +1,707 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ QODER Fill Prototype - 用真实数据自动预填原型模板 (80%完成)
5
+
6
+ 解决 AI 出原型时"从零手写 + 发明颜色"的效率问题:
7
+ - 从 style-index 取真实表格列/表单字段 (不再 AI 编)
8
+ - 从 field_map 补充相关字段的中文标题
9
+ - token 直接继承模板 (模板已有正确 HSL 值, 不碰颜色)
10
+ - 输出 80% 完成的 HTML 草稿 + 数据清单
11
+
12
+ AI 只需做 20% 微调 (交互细节/布局调整), 不再重写。
13
+
14
+ Usage:
15
+ python fill_prototype.py <关键词> [--platform web|app] [--type table|form]
16
+ python fill_prototype.py 营业外合同 --platform web
17
+ """
18
+
19
+ import os
20
+ import sys
21
+ import json
22
+ import re
23
+ import argparse
24
+
25
+ # UTF-8 stdio
26
+ try:
27
+ sys.stdout.reconfigure(encoding='utf-8', errors='replace')
28
+ except (AttributeError, TypeError, OSError, IOError):
29
+ try:
30
+ sys.stdout.reconfigure(encoding='utf-8')
31
+ except Exception:
32
+ pass
33
+
34
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
35
+ from common.terms import expand_chinese_query, get_platform_map
36
+
37
+ BASE = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
38
+ INDEX_DIR = os.path.join(BASE, 'data', 'index')
39
+ TEMPLATE_DIR = os.path.join(BASE, '.qoder', 'templates')
40
+
41
+
42
+ def load_index(filename):
43
+ """Load index from DuckDB (preferred) or JSON fallback."""
44
+ try:
45
+ from search_index import _duckdb_load
46
+ result = _duckdb_load(filename)
47
+ if result is not None:
48
+ return result
49
+ except Exception:
50
+ pass
51
+ path = os.path.join(INDEX_DIR, filename)
52
+ if not os.path.isfile(path):
53
+ return None
54
+ try:
55
+ with open(path, encoding='utf-8') as f:
56
+ return json.load(f)
57
+ except Exception:
58
+ return None
59
+
60
+
61
+ def extract_real_buttons(query_words, limit=6):
62
+ """从 entity-registry.json 提取真实按钮 (按钮文案 + 关联 API)。
63
+
64
+ 这让原型显示系统里真实的按钮文案 ("添加反馈资产" "导出Excel"),
65
+ 而不是 AI 编的 ("新增" "编辑")。按钮名来自真实 Vue 代码。
66
+ """
67
+ er = load_index('entity-registry.json')
68
+ if not er or not isinstance(er, dict):
69
+ return []
70
+ entities = er.get('entities', {})
71
+ if not entities:
72
+ return []
73
+ buttons = []
74
+ query_lower = [w.lower() for w in query_words]
75
+ for eid, info in entities.items():
76
+ if not eid.startswith('B:'):
77
+ continue
78
+ props = info.get('properties', {}) if isinstance(info, dict) else {}
79
+ btn_text = props.get('button_text', '')
80
+ endpoint = props.get('endpoint', '')
81
+ vue_file = props.get('vue_file', '') or props.get('feature', '')
82
+ # 关键词命中: 按钮文案或文件路径包含查询词
83
+ match_text = (btn_text + ' ' + vue_file).lower()
84
+ if any(w in match_text for w in query_lower):
85
+ buttons.append({'text': btn_text, 'endpoint': endpoint, 'source': eid[2:][:60]})
86
+ if len(buttons) >= limit:
87
+ break
88
+ return buttons
89
+
90
+
91
+ def find_best_example(query_words, style_index, platform, page_type='table-page'):
92
+ """从 style-index 找最相似的页面 (table_columns/form_fields 最多)。
93
+
94
+ 返回 (example_dict, source_file) 或 (None, None)。
95
+ """
96
+ proj_map = get_platform_map()
97
+ target_projects = proj_map.get(platform, [])
98
+ if not target_projects:
99
+ target_projects = list((style_index.get('projects') or {}).keys())
100
+
101
+ best = None
102
+ best_score = 0
103
+ best_file = None
104
+
105
+ for proj_name, pdata in (style_index.get('projects') or {}).items():
106
+ if target_projects and proj_name not in target_projects:
107
+ continue
108
+ for pt, examples in (pdata.get('page_examples') or {}).items():
109
+ if page_type and pt != page_type:
110
+ continue
111
+ for ex in examples:
112
+ if not isinstance(ex, dict):
113
+ continue
114
+ cols = ex.get('table_columns', [])
115
+ fields = ex.get('form_fields', [])
116
+ score = len(cols) + len(fields)
117
+
118
+ # 关键词匹配加分: 字段名或标题包含查询词
119
+ all_text = ' '.join(
120
+ str(c.get('field', '')) + ' ' + str(c.get('title', ''))
121
+ for c in cols
122
+ ) + ' ' + ' '.join(
123
+ str(f.get('field', '')) + ' ' + str(f.get('label', ''))
124
+ for f in fields
125
+ ) + ' ' + ex.get('file', '')
126
+ all_lower = all_text.lower()
127
+ for w in query_words:
128
+ if w.lower() in all_lower:
129
+ score += 5 # 关键词命中加分
130
+
131
+ if score > best_score:
132
+ best_score = score
133
+ best = ex
134
+ best_file = ex.get('file', '')
135
+
136
+ return best, best_file
137
+
138
+
139
+ def supplement_fields_from_map(query_words, field_map, existing_fields, limit=6):
140
+ """从 field_map 补充页面里没有但相关的字段 (取中文标题)。
141
+
142
+ field_map 格式: {field_name: {titles: set, files: list}}
143
+ """
144
+ extra = []
145
+ existing_names = set()
146
+ for f in existing_fields:
147
+ existing_names.add(f.get('field', '').lower())
148
+ existing_names.add(f.get('label', '').lower())
149
+
150
+ for field_name, info in field_map.items():
151
+ if field_name.lower() in existing_names:
152
+ continue
153
+ titles = info.get('titles', set()) if isinstance(info.get('titles'), set) else set(info.get('titles', []))
154
+ title_str = ' '.join(str(t) for t in titles)
155
+ # 关键词命中 (字段名或标题含查询词)
156
+ match_text = (field_name + ' ' + title_str).lower()
157
+ for w in query_words:
158
+ if w.lower() in match_text:
159
+ title = next(iter(titles), field_name) if titles else field_name
160
+ extra.append({'field': field_name, 'title': title})
161
+ break
162
+ if len(extra) >= limit:
163
+ break
164
+
165
+ return extra
166
+
167
+
168
+ def gen_search_form_html(form_fields, extra_fields):
169
+ """生成搜索表单 HTML (从真实 form_fields)。"""
170
+ if not form_fields and not extra_fields:
171
+ return '<div class="form-item"><label>关键词</label><input type="text" placeholder="请输入"></div>'
172
+
173
+ items = []
174
+ for f in (form_fields + extra_fields)[:6]: # 搜索表单最多 6 个字段
175
+ label = f.get('label') or f.get('title') or f.get('field', '')
176
+ field = f.get('field', '')
177
+ items.append(
178
+ ' <div class="form-item">\n'
179
+ ' <label>{label}</label>\n'
180
+ ' <input type="text" placeholder="请输入{label}" data-field="{field}">\n'
181
+ ' </div>'.format(label=label, field=field)
182
+ )
183
+ items.append(
184
+ ' <div class="form-item">\n'
185
+ ' <button class="btn btn-primary">查询</button>\n'
186
+ ' <button class="btn btn-outline">重置</button>\n'
187
+ ' </div>'
188
+ )
189
+ return '\n'.join(items)
190
+
191
+
192
+ def gen_table_html(table_columns, extra_fields):
193
+ """生成表头 + 3 行示例数据 HTML (从真实 table_columns)。"""
194
+ all_cols = table_columns + [{'field': e['field'], 'title': e['title']} for e in extra_fields]
195
+ all_cols = all_cols[:12] # 表格最多 12 列
196
+
197
+ # 表头
198
+ headers = []
199
+ for col in all_cols:
200
+ title = col.get('title') or col.get('field', '')
201
+ headers.append(' <th>{}</th>'.format(title))
202
+ headers_html = '\n'.join(headers)
203
+
204
+ # 3 行示例数据
205
+ rows = []
206
+ for i in range(1, 4):
207
+ cells = []
208
+ for col in all_cols:
209
+ field = col.get('field', '')
210
+ title = col.get('title', '')
211
+ # 示例数据: 用字段名 + 行号生成假数据, 让 AI 知道每列是什么
212
+ sample = '{}示例{}'.format(title[:4], i) if title else '{}-{}'.format(field[:6], i)
213
+ cells.append(' <td>{}</td>'.format(sample))
214
+ # 加操作列 (用真实按钮文案, 来自 entity-registry; 没有则用通用文案)
215
+ cells.append(' <td><a href="#">详情</a> <a href="#">编辑</a></td>')
216
+ rows.append(' <tr>\n' + '\n'.join(cells) + '\n </tr>')
217
+
218
+ rows_html = '\n'.join(rows)
219
+
220
+ # 操作列表头
221
+ headers_html += '\n <th>操作</th>'
222
+
223
+ return headers_html, rows_html
224
+
225
+
226
+ def infer_module_name(source_file):
227
+ """从源文件路径推断模块名。"""
228
+ if not source_file:
229
+ return '业务模块'
230
+ # data/code/fywl-ui/apps/ics/src/views/HR/... → HR
231
+ parts = source_file.replace('\\', '/').split('/')
232
+ for i, p in enumerate(parts):
233
+ if p == 'views' and i + 1 < len(parts):
234
+ return parts[i + 1]
235
+ return parts[-2] if len(parts) >= 2 else '业务模块'
236
+
237
+
238
+ def resolve_module_name(query, source_file, platform):
239
+ """解析模块名 (优先用 module-index 真实名, 回退路径推断)。
240
+
241
+ ui-modules.json 由 build_style_index.py 生成, 含真实业务模块的
242
+ 英文目录名 + 中文标签。先按 query 关键词匹配模块中文名, 再按 source_file
243
+ 的路径片段匹配目录名; 都不中则回退 infer_module_name。
244
+ 返回 (module_cn, module_dir)。
245
+ """
246
+ # 1. 加载 module-index
247
+ mod_index = load_index('ui-modules.json') or {}
248
+ proj_map = get_platform_map()
249
+ proj_name = proj_map.get(platform)
250
+ mods = (mod_index.get('projects') or {}).get(proj_name, [])
251
+ if not mods:
252
+ # 项目无模块索引, 回退
253
+ return infer_module_name(source_file), None
254
+
255
+ # 2. 按 query 关键词匹配中文标签 (如 "车辆管理" -> 匹配含"管理"的模块)
256
+ q = (query or '').lower()
257
+ if q:
258
+ # 精确 / 包含匹配 (最稳)
259
+ for m in mods:
260
+ if m['cn'].lower() == q or q in m['cn'].lower():
261
+ return m['cn'], m['dir']
262
+ if m['dir'].lower() in q or q in m['dir'].lower():
263
+ return m['cn'], m['dir']
264
+ # 宽松: 共享 2+ 字前缀 (车辆管理 -> 车辆生活)
265
+ for m in mods:
266
+ cn = m['cn']
267
+ shared = sum(1 for i in range(min(len(q), len(cn))) if q[i] == cn[i].lower())
268
+ if shared >= 2 and len(q) >= 2:
269
+ return m['cn'], m['dir']
270
+
271
+ # 3. 按 source_file 路径片段匹配目录名
272
+ if source_file:
273
+ sf_lower = source_file.replace('\\', '/').lower()
274
+ for m in mods:
275
+ if '/' + m['dir'].lower() + '/' in sf_lower:
276
+ return m['cn'], m['dir']
277
+
278
+ # 4. 回退: 路径推断 (但用真实模块中文名校正 views/ 后的目录)
279
+ guessed = infer_module_name(source_file)
280
+ for m in mods:
281
+ if m['dir'].lower() == guessed.lower():
282
+ return m['cn'], m['dir']
283
+ return guessed, None
284
+
285
+
286
+ def get_layout_fingerprint(style_index, platform):
287
+ """读取目标项目的布局指纹 (build_style_index.py 提取)。
288
+
289
+ 返回 fingerprint dict;若无则返回 None。用于让原型草稿贴合目标项目的
290
+ 真实外壳 (如 ICS = mixed-nav 后端路由多级菜单)。
291
+ """
292
+ proj_map = get_platform_map()
293
+ proj_name = proj_map.get(platform)
294
+ projects = style_index.get('projects') or {}
295
+ if proj_name and proj_name in projects:
296
+ return projects[proj_name].get('layout_fingerprint')
297
+ return None
298
+
299
+
300
+ def gen_mixed_nav_menu(module_name, page_name, sibling_modules=None, logo_text=None):
301
+ """为 mixed-nav 布局生成示例菜单骨架 (一级顶栏 + 二级侧栏 + 标签页)。
302
+
303
+ sibling_modules: 同项目的其他真实模块 [(cn, dir), ...], 若提供则用真实
304
+ 模块名填充顶部一级菜单 (当前模块 active), 替代占位文字。
305
+ logo_text: 顶部 logo 文字, 默认 '管理平台' (通用, 不绑定具体项目)。
306
+ 返回 (top_menu, sidebar_menu, tabs, logo_text)。
307
+ """
308
+ if not logo_text:
309
+ logo_text = '管理平台'
310
+ # 顶部一级菜单: 优先用真实兄弟模块名, 当前模块 active
311
+ if sibling_modules:
312
+ others = [cn for cn, _ in sibling_modules if cn != module_name][:3]
313
+ items = ['<div class="top-menu-item">%s</div>' % cn for cn in others[:2]]
314
+ items.append('<div class="top-menu-item active">%s</div>' % module_name)
315
+ if len(others) > 2:
316
+ items.append('<div class="top-menu-item">%s</div>' % others[2])
317
+ top_menu = '\n '.join(items)
318
+ else:
319
+ top_menu = (
320
+ '<div class="top-menu-item">概览</div>\n'
321
+ ' <div class="top-menu-item active">{mod}</div>\n'
322
+ ' <div class="top-menu-item">系统管理</div>'
323
+ ).format(mod=module_name)
324
+ # 左侧二级 + 三级折叠示例 (当前页 active)
325
+ sidebar_menu = (
326
+ '<div class="sidebar-item active">{page}</div>\n'
327
+ ' <details class="sidebar-group">\n'
328
+ ' <summary>二级菜单A</summary>\n'
329
+ ' <div class="sidebar-sub">\n'
330
+ ' <div class="sidebar-item">三级菜单A-1</div>\n'
331
+ ' <div class="sidebar-item">三级菜单A-2</div>\n'
332
+ ' </div>\n'
333
+ ' </details>\n'
334
+ ' <div class="sidebar-item">二级菜单B</div>'
335
+ ).format(page=page_name)
336
+ # 标签页 (承载三四级)
337
+ tabs = (
338
+ '<div class="tab-item active">{page} <span class="tab-close">×</span></div>'
339
+ ).format(page=page_name)
340
+ return top_menu, sidebar_menu, tabs, logo_text
341
+
342
+
343
+ def load_design_spec(query, platform=None):
344
+ """读取与当前需求匹配的设计规范 spec.json (design-import 录入)。
345
+
346
+ 扫描 data/style/*-design-spec.json (兼容旧 {name}- 和新 {name}-{平台}- 命名),
347
+ 优先匹配 requirement 字段; 同分时优先选 platform 匹配的; 仍同分按导入时间取最新。
348
+ 目录为空返回 None。
349
+
350
+ spec 优先级 > 代码风格指纹 > 模板默认值 (design-import SKILL.md 铁律)。
351
+ """
352
+ style_dir = os.path.join(BASE, 'data', 'style')
353
+ if not os.path.isdir(style_dir):
354
+ return None
355
+ candidates = []
356
+ for f in os.listdir(style_dir):
357
+ if not f.endswith('-design-spec.json'):
358
+ continue
359
+ path = os.path.join(style_dir, f)
360
+ try:
361
+ with open(path, encoding='utf-8') as fh:
362
+ spec = json.load(fh)
363
+ except Exception:
364
+ continue
365
+ req = (spec.get('requirement') or '').lower()
366
+ q = (query or '').lower()
367
+ spec_plat = (spec.get('platform') or '').lower()
368
+ # 匹配分: requirement 命中得高分, 否则低分
369
+ req_score = 10 if (q and q in req) else (5 if req else 0)
370
+ # 平台匹配加分 (同平台 spec 优先, 避免误用另一端的颜色)
371
+ plat_score = 3 if (platform and spec_plat == platform.lower()) else 0
372
+ score = req_score + plat_score
373
+ candidates.append((score, spec.get('imported_at', ''), spec))
374
+ if not candidates:
375
+ return None
376
+ candidates.sort(key=lambda x: (x[0], x[1]), reverse=True)
377
+ return candidates[0][2]
378
+
379
+
380
+ def gen_menu_from_spec(spec, module_name, page_name):
381
+ """用 design-import 的 spec.layout/components 生成菜单骨架。
382
+
383
+ 解析 spec 里关于侧边栏/菜单的自然语言描述 (如 "三级折叠, 二级手风琴",
384
+ "一级固定"), 生成对应 HTML 骨架。返回 (top_menu, sidebar_menu, tabs, logo_text)。
385
+ """
386
+ layout = spec.get('layout') or {}
387
+ comps = spec.get('components') or []
388
+ # 合并所有描述文本做关键词探测
389
+ desc_text = ' '.join([
390
+ layout.get('description', ''), layout.get('sidebar', ''), layout.get('content', '')
391
+ ] + [c.get('spec', '') for c in comps]).lower()
392
+
393
+ has_accordion = '手风琴' in desc_text or 'accordion' in desc_text
394
+ has_three = '三级' in desc_text or '3级' in desc_text
395
+ has_two = '二级' in desc_text or '2级' in desc_text
396
+
397
+ logo_text = '管理平台'
398
+ top_menu = (
399
+ '<div class="top-menu-item">{mod}</div>\n'
400
+ ' <div class="top-menu-item active">{mod}</div>'
401
+ ).format(mod=module_name)
402
+
403
+ # 侧边栏: 根据描述决定层级深度
404
+ parts = ['<div class="sidebar-item active">{page}</div>'.format(page=page_name)]
405
+ if has_three or has_accordion:
406
+ # 三级折叠 (spec 明确要求时) — 默认展开第一个
407
+ open_attr = ' open' if has_accordion else ''
408
+ parts.append(
409
+ '<details class="sidebar-group"{open}>\n'
410
+ ' <summary>二级菜单 (手风琴)</summary>\n'
411
+ ' <div class="sidebar-sub">\n'
412
+ ' <div class="sidebar-item">三级菜单-1</div>\n'
413
+ ' <div class="sidebar-item">三级菜单-2</div>\n'
414
+ ' </div>\n'
415
+ ' </details>'.format(open=open_attr)
416
+ )
417
+ elif has_two:
418
+ parts.append('<div class="sidebar-item">二级菜单</div>')
419
+ sidebar_menu = '\n '.join(parts)
420
+
421
+ tabs = '<div class="tab-item active">{page} <span class="tab-close">×</span></div>'.format(page=page_name)
422
+ return top_menu, sidebar_menu, tabs, logo_text
423
+
424
+
425
+ def gen_app_tabbar(query, sibling_modules=None):
426
+ """为 APP (Vant) 生成底部 TabBar (真实模块名 + Vant 字体图标)。
427
+
428
+ sibling_modules: [(cn, dir), ...] 来自 module-index。当前模块 active。
429
+ 返回 tabbar HTML 字符串 (用 van-icon-* 字体图标, 绝不用 emoji)。
430
+ """
431
+ # 选 4 个 tab: 首页 + 当前模块 + 1-2 个相关模块 + 我的
432
+ others = [cn for cn, _ in (sibling_modules or []) if cn != query][:2]
433
+ items = ['<div class="tab-item active">\n <i class="van-icon van-icon-wap-home-o tab-icon"></i><span>首页</span>\n </div>']
434
+ if query:
435
+ items.append('<div class="tab-item">\n <i class="van-icon van-icon-apps-o tab-icon"></i><span>%s</span>\n </div>' % query)
436
+ for cn in others:
437
+ icon = 'van-icon-chart-trend-o' if '统计' in cn or '看板' in cn else 'van-icon-list-switching'
438
+ items.append('<div class="tab-item">\n <i class="van-icon %s tab-icon"></i><span>%s</span>\n </div>' % (icon, cn))
439
+ items.append('<div class="tab-item">\n <i class="van-icon van-icon-user-o tab-icon"></i><span>我的</span>\n </div>')
440
+ return '\n '.join(items[:4])
441
+
442
+
443
+ def main():
444
+ parser = argparse.ArgumentParser(description='用真实数据预填原型模板')
445
+ parser.add_argument('query', help='业务关键词 (如: 营业外合同)')
446
+ parser.add_argument('--platform', default='web', choices=['web', 'app'],
447
+ help='平台 (默认 web)')
448
+ parser.add_argument('--type', default='table-page',
449
+ choices=['table-page', 'form-page', 'detail-page', 'modal', 'dashboard'],
450
+ help='页面类型 (默认 table-page)')
451
+ args = parser.parse_args()
452
+
453
+ # 平台守卫: 注意 --platform 有 default='web', 不能查 args.platform (永远非 None),
454
+ # 必须查 sys.argv 是否【显式】传了 --platform。否则违规调用会被默认值绕过。
455
+ from common.platform_guard import assert_platform_selected
456
+ assert_platform_selected(skill='wl-prd-full')
457
+
458
+ query = args.query.strip()
459
+ if not query:
460
+ print('Error: 关键词必填', file=sys.stderr)
461
+ return 1
462
+
463
+ # 1. 扩展查询词
464
+ words = expand_chinese_query(query)
465
+ if not words:
466
+ words = [query.lower()]
467
+ words_lower = [w.lower() for w in words]
468
+
469
+ # 2. 加载索引
470
+ style_index = load_index('ui-style.json') or {}
471
+ field_map = style_index.get('field_map') or {}
472
+
473
+ # 3. 找最佳参考页面
474
+ best_example, source_file = find_best_example(
475
+ words_lower, style_index, args.platform, args.type)
476
+
477
+ table_columns = []
478
+ form_fields = []
479
+ if best_example:
480
+ table_columns = best_example.get('table_columns', [])
481
+ form_fields = best_example.get('form_fields', [])
482
+
483
+ # 4. 从 field_map 补充相关字段
484
+ extra_fields = supplement_fields_from_map(words_lower, field_map, table_columns + form_fields)
485
+
486
+ # 4.5 从 entity-registry 提取真实按钮 (原型显示系统真实按钮文案, 不是 AI 编的)
487
+ real_buttons = extract_real_buttons(words_lower)
488
+
489
+ # 5. 读模板
490
+ template_name = 'prototype-web.html' if args.platform == 'web' else 'prototype-app.html'
491
+ template_path = os.path.join(TEMPLATE_DIR, template_name)
492
+ if not os.path.isfile(template_path):
493
+ print('Error: 模板不存在: {}'.format(template_path), file=sys.stderr)
494
+ return 1
495
+ with open(template_path, encoding='utf-8') as f:
496
+ template = f.read()
497
+
498
+ # 6. 填模板
499
+ module_name, module_dir = resolve_module_name(query, source_file, args.platform)
500
+ search_form_html = gen_search_form_html(form_fields, extra_fields)
501
+ headers_html, rows_html = gen_table_html(table_columns, extra_fields)
502
+
503
+ # 菜单骨架优先级: design-import spec > 布局指纹(mixed-nav) > 技术栈推测 > 诚实降级
504
+ # (design-import SKILL.md:72 "spec 优先级 > 代码风格 > PDF")
505
+ spec = load_design_spec(query, args.platform)
506
+ fp = get_layout_fingerprint(style_index, args.platform)
507
+ stack = (fp or {}).get('stack', 'unknown')
508
+ style_warning = '' # 非空时表示风格不可靠, 需提示用户
509
+
510
+ if spec:
511
+ # 有设计规格: 用 spec 驱动菜单骨架 + token
512
+ top_menu, sidebar_menu, tabs, logo_text = gen_menu_from_spec(
513
+ spec, module_name, query)
514
+ elif fp and fp.get('layout') == 'mixed-nav':
515
+ # 无 spec 但项目是 mixed-nav (如 ICS): 用指纹示例骨架 + 真实兄弟模块名
516
+ mod_index = load_index('ui-modules.json') or {}
517
+ proj_name = get_platform_map().get(args.platform)
518
+ mods = (mod_index.get('projects') or {}).get(proj_name, [])
519
+ siblings = [(m['cn'], m['dir']) for m in mods if m.get('cn') != module_name]
520
+ top_menu, sidebar_menu, tabs, logo_text = gen_mixed_nav_menu(
521
+ module_name, query, siblings)
522
+ elif stack in ('vben-admin', 'ant-design-vue', 'element-plus', 'vant-mobile'):
523
+ # 无布局指纹但检测到技术栈: 给推测骨架, 标记低置信度让 AI 知道这是猜测
524
+ if args.platform == 'app' or stack == 'vant-mobile':
525
+ top_menu, sidebar_menu, tabs = '', '', ''
526
+ logo_text = '管理平台'
527
+ else:
528
+ top_menu, sidebar_menu, tabs, logo_text = gen_mixed_nav_menu(
529
+ module_name, query)
530
+ style_warning = (
531
+ '检测到技术栈: {} — 但没有 preferences.ts/设计稿, 骨架基于推测 (低置信度)。'
532
+ '请提供 1 个现有页面 URL/截图/设计稿, 我才能贴合你的真实风格。'.format(stack)
533
+ )
534
+ else:
535
+ # 完全未知: 不硬画, 诚实提示
536
+ top_menu, sidebar_menu, tabs, logo_text = '', '', '', '管理平台'
537
+ style_warning = (
538
+ '未检测到技术栈和风格参考 (stack={})。原型用通用占位骨架, 请勿直接使用。'
539
+ '请提供现有页面 URL/截图/设计稿, 或用 /design-import 录入设计规格。'.format(stack)
540
+ )
541
+ is_mixed = bool(spec or (fp and fp.get('layout') == 'mixed-nav'))
542
+
543
+ # 若 spec 提供了 design_tokens (--primary-color 等), 覆盖模板 :root
544
+ spec_tokens_html = ''
545
+ if spec and spec.get('design_tokens'):
546
+ token_lines = []
547
+ for var, val in spec['design_tokens'].items():
548
+ # spec 用 CSS 变量名 (--primary-color), 模板用 HSL 分量 (--primary)
549
+ # 直接注入额外变量, AI 微调时按需映射
550
+ token_lines.append(' %s: %s;' % (var, val))
551
+ if token_lines:
552
+ spec_tokens_html = ' /* design-import spec tokens (优先级最高) */\n' + '\n'.join(token_lines) + '\n'
553
+
554
+ filled = template
555
+ # 注入 spec token (若有) 到 :root 末尾
556
+ if spec_tokens_html:
557
+ # 锚点优先级: 通用锚点 (app+web 都有) > web 专用 mixed-nav > 兜底 :root 闭合
558
+ # (历史 bug: 旧版只认 mixed-nav 锚点, app 模板没有 → APP 端 spec 注入静默失败)
559
+ injected = False
560
+ for anchor in (' /* design-import spec tokens anchor',
561
+ ' /* mixed-nav shell widths'):
562
+ if anchor in filled:
563
+ filled = filled.replace(anchor, spec_tokens_html + '\n' + anchor, 1)
564
+ injected = True
565
+ break
566
+ if not injected:
567
+ # 兜底: 在第一个 :root { ... } 的闭合 } 前注入 (正则, 容错)
568
+ import re as _re
569
+ filled = _re.sub(r'(\s*:root\s*\{[^}]*?)(\n \})',
570
+ lambda m: m.group(1) + '\n' + spec_tokens_html.rstrip() + m.group(2),
571
+ filled, count=1)
572
+ # inject layout_fingerprint real layout params (sidebar/topbar width)
573
+ # so AI does not guess widths - uses real values from ui-style.json
574
+ if fp:
575
+ _fp_ov = []
576
+ if str(fp.get('sidebar_width', '')).strip().isdigit():
577
+ _fp_ov.append(' --sidebar-width: {}px;'.format(fp['sidebar_width']))
578
+ if str(fp.get('topbar_height', '')).strip().isdigit():
579
+ _fp_ov.append(' --topbar-height: {}px;'.format(fp['topbar_height']))
580
+ if _fp_ov:
581
+ _fp_h = ' /* layout_fingerprint real layout (from ui-style.json) */' + chr(10) + chr(10).join(_fp_ov) + chr(10)
582
+ filled = filled.replace(' /* mixed-nav shell widths', _fp_h + ' /* mixed-nav shell widths')
583
+ filled = filled.replace('{{TITLE}}', query)
584
+ filled = filled.replace('{{LOGO_TEXT}}', logo_text)
585
+ filled = filled.replace('{{MODULE_NAME}}', module_name)
586
+ filled = filled.replace('{{PAGE_NAME}}', query)
587
+ filled = filled.replace('{{TOP_MENU}}', top_menu)
588
+ filled = filled.replace('{{SIDEBAR_MENU}}', sidebar_menu)
589
+ filled = filled.replace('{{TABS}}', tabs)
590
+ filled = filled.replace('{{SEARCH_FORM_FIELDS}}', search_form_html)
591
+ filled = filled.replace('{{TABLE_HEADERS}}', headers_html)
592
+ filled = filled.replace('{{TABLE_ROWS}}', rows_html)
593
+
594
+ # APP 专属占位符 (Vant tabbar + 主色)
595
+ if args.platform == 'app':
596
+ mod_index = load_index('ui-modules.json') or {}
597
+ proj_name = get_platform_map().get(args.platform)
598
+ mods = (mod_index.get('projects') or {}).get(proj_name, [])
599
+ siblings = [(m['cn'], m['dir']) for m in mods]
600
+ tabbar_html = gen_app_tabbar(module_name, siblings)
601
+ filled = filled.replace('{{TAB_BAR_ITEMS}}', tabbar_html)
602
+ filled = filled.replace('{{APP_PRIMARY}}', '#1989fa')
603
+ else:
604
+ # Web 模板无这些占位符, 无副作用
605
+ filled = filled.replace('{{TAB_BAR_ITEMS}}', '')
606
+ filled = filled.replace('{{APP_PRIMARY}}', '#1989fa')
607
+
608
+ # 7. 输出数据清单 (让 AI 知道用了什么真实数据)
609
+ print('=' * 60)
610
+ print('原型预填完成: {} ({} {})'.format(query, args.platform, args.type))
611
+ print('=' * 60)
612
+ print()
613
+ print('## 数据清单 (所有数据来自真实代码, 非编造)')
614
+ print()
615
+ if source_file:
616
+ print('参考页面: {}'.format(source_file))
617
+ else:
618
+ print('参考页面: (无精确匹配, 用模板默认值)')
619
+ print()
620
+
621
+ print('表格列 ({} 个, 来自真实代码):'.format(len(table_columns)))
622
+ for col in table_columns[:8]:
623
+ print(' - {} ({})'.format(col.get('title', '?'), col.get('field', '?')))
624
+ if len(table_columns) > 8:
625
+ print(' ... 共 {} 个'.format(len(table_columns)))
626
+ print()
627
+
628
+ print('表单字段 ({} 个):'.format(len(form_fields)))
629
+ for f in form_fields[:6]:
630
+ print(' - {} ({})'.format(f.get('label', '?'), f.get('field', '?')))
631
+ print()
632
+
633
+ if extra_fields:
634
+ print('field_map 补充字段 ({} 个, 关键词匹配):'.format(len(extra_fields)))
635
+ for e in extra_fields:
636
+ print(' - {} ({})'.format(e['title'], e['field']))
637
+ print()
638
+
639
+ # 真实按钮 (来自 entity-registry, 原型用这些文案而非 AI 编的)
640
+ if real_buttons:
641
+ print('真实按钮 ({} 个, 来自 entity-registry → 原型用这些文案):'.format(len(real_buttons)))
642
+ for b in real_buttons:
643
+ endpoint_str = ' → ' + b['endpoint'] if b['endpoint'] else ''
644
+ print(' - {}{}'.format(b['text'], endpoint_str))
645
+ print()
646
+ else:
647
+ print('真实按钮: (无精确匹配, 用通用文案: 新增/导出/导入/批量操作)')
648
+ print()
649
+
650
+ # 设计规格 (design-import spec) — 最高优先级, 有就用它
651
+ if spec:
652
+ print('★ 设计规格 (design-import spec, 优先级最高):')
653
+ print(' 来源: {}'.format(spec.get('source', '?')))
654
+ print(' 需求: {}'.format(spec.get('requirement', '?')))
655
+ layout = spec.get('layout') or {}
656
+ if layout.get('description'):
657
+ print(' 布局: {}'.format(layout['description']))
658
+ comps = spec.get('components') or []
659
+ for c in comps:
660
+ if '菜单' in c.get('name', '') or 'menu' in c.get('name', '').lower():
661
+ print(' 菜单组件: {}'.format(c.get('spec', '')))
662
+ if spec.get('design_tokens'):
663
+ print(' 自定义 token 已注入模板 :root (见 HTML 注释)')
664
+ print(' ⚠️ 已按 spec 生成菜单骨架, 微调时保持 spec 描述的层级/折叠方式')
665
+ print()
666
+
667
+ # 风格不可靠警告 (技术栈推测或完全未知时)
668
+ if style_warning:
669
+ print('⚠️ 风格置信度低:')
670
+ print(' ' + style_warning)
671
+ print()
672
+
673
+ print('设计 Token: 继承模板 ({})'.format(template_name))
674
+ if fp:
675
+ print(' 布局模式: {} (来自 style-index layout_fingerprint)'.format(fp.get('layout', '?')))
676
+ print(' 路由模式: {} (backend=菜单来自后端接口, 需按 PRD 填菜单树)'.format(fp.get('access_mode', '?')))
677
+ if fp.get('sidebar_width'):
678
+ print(' 侧边栏宽度: {}px'.format(fp['sidebar_width']))
679
+ if fp.get('color_primary'):
680
+ print(' 主色: {} (Vben HSL, 已写入模板 :root, 勿改)'.format(fp['color_primary']))
681
+ print(' 禁止: #1890ff (Ant Design 默认色, 非 Vben)')
682
+ elif args.platform == 'web':
683
+ print(' 主色: hsl(214.58 86.34% 59.8%) (Vben, 模板内置, 勿改)')
684
+ print(' 禁止: #1890ff (Ant Design 默认色, 非 Vben)')
685
+ print()
686
+ print('## AI 微调指南 (你只需做 20%)')
687
+ print(' 1. 确认表格列/字段是否符合需求 (增删列)')
688
+ if is_mixed:
689
+ print(' 2. ⚠️ 菜单层级: 模板已填 mixed-nav 示例骨架 (一级顶栏/二级侧栏/三级折叠/标签页)。')
690
+ print(' 若 PRD 描述了侧边栏/菜单结构, 按它替换示例菜单, 不要退化成扁平单级!')
691
+ print(' 3. 补充交互细节 (弹窗/Tab切换/三级折叠)')
692
+ else:
693
+ print(' 2. 补充交互细节 (如侧边栏折叠/弹窗/Tab切换)')
694
+ print(' 4. ⚠️ token (颜色/间距) 已正确, 不要改!')
695
+ print()
696
+
697
+ # 8. 输出 HTML 草稿 (用分隔符标记, AI 能识别)
698
+ print('## 预填 HTML 草稿 (80% 完成, 复制后微调)')
699
+ print('```html')
700
+ print(filled)
701
+ print('```')
702
+
703
+ return 0
704
+
705
+
706
+ if __name__ == '__main__':
707
+ sys.exit(main())