@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
@@ -0,0 +1,359 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ # v3.0 路径自举: 引导到 common/bootstrap, 统一 sys.path 逻辑
4
+ import os as _o, sys as _s
5
+ _cp = _o.path.join(_o.path.dirname(_o.path.dirname(_o.path.abspath(__file__))), 'common')
6
+ if _cp not in _s.path: _s.path.insert(0, _cp)
7
+ from bootstrap import setup; setup()
8
+
9
+ """
10
+ QODER Prefetch — 需求上下文"预测 + 批量预取"
11
+
12
+ 治的病: 写 PRD 时串行试探式搜索 (查一个词不命中 → 换词再查 → 再换...),
13
+ 一次需求 6+ 次往返, 又慢又可能违反"禁止 grep 全库"的规则。
14
+
15
+ 做法 (用户原话): "开始前先预测需要查哪些, 一批查完, 不够再补。"
16
+
17
+ 输入一句需求描述, 输出一份合并去重的 context_pack (8 段), 等价于把多个
18
+ 业务词的查询结果一次性合并。一次调用替代 6+ 次串行 search/find/grep。
19
+
20
+ Usage:
21
+ python prefetch.py "保险单OCR识别标注对比图" --platform web
22
+ python prefetch.py "考勤导出按钮" --platform app
23
+
24
+ 设计:
25
+ - 不重新实现 context_pack 的 8 段, 复用其底层函数 (load/match_kw/plat_filter)
26
+ - 词预测: 从需求句抽业务实体 + expand_chinese_query 分词 + 缩写直通
27
+ - 多词命中合并去重, 按"被几个词命中"排序 (多词命中的文件排前面)
28
+ - 降级: 词预测失败 → 回退单词 context_pack
29
+ """
30
+
31
+ import os
32
+ import sys
33
+
34
+ # UTF-8 stdio (Windows 终端默认 GBK)
35
+ try:
36
+ sys.stdout.reconfigure(encoding='utf-8', errors='replace')
37
+ except (AttributeError, TypeError, OSError, IOError):
38
+ pass
39
+
40
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
41
+
42
+ # 复用 context_pack 的底层 (单一数据源, 不重复造轮子)
43
+ from context_pack import (
44
+ load, match_kw, plat_filter, PLATFORM_MAP, MIN_FUZZY_LEN,
45
+ BASE, INDEX_DIR, BUILD_CACHE_DIR,
46
+ )
47
+ from common.terms import expand_chinese_query, get_cn_map_with_auto, BUSINESS_PATH_MAP
48
+
49
+
50
+ # ── 词预测 ──────────────────────────────────────────────────────────
51
+
52
+ # 常见缩写/英文技术词, 不在 CN_MAP 里, 直接作为英文词参与子串匹配
53
+ # (OCR 这种缩写原来直接原样返回 [ocr], 在 code-keyword 里 0 命中)
54
+ _EN_ACRONYMS = {
55
+ 'ocr', 'api', 'gps', 'sms', 'qrcode', 'qr', 'pdf', 'excel',
56
+ 'barcode', 'rfid', 'nfc', 'ai', 'etl', 'crm', 'erp', 'oa',
57
+ }
58
+
59
+
60
+ def predict_words(sentence):
61
+ """从一句需求描述预测出要查的业务词集合。
62
+
63
+ 三级抽取:
64
+ 1. 直接命中 CN_MAP/BUSINESS_PATH_MAP 的中文词 (保险/考勤/导出...)
65
+ 2. 英文缩写/技术词 (OCR/API/GPS...) 直通
66
+ 3. 句子里其它 >=2 字的片段, 兜底参与匹配
67
+
68
+ 返回 (主词列表, 用于命中的英文小写词列表)。主词是中文业务词,
69
+ 英文词用于索引子串匹配。
70
+ """
71
+ sentence = sentence.strip()
72
+ if not sentence:
73
+ return [], []
74
+
75
+ cn_map = get_cn_map_with_auto()
76
+ primary_cn = [] # 命中的中文业务词 (用于展示)
77
+ en_words = set() # 所有英文搜索词 (用于命中匹配)
78
+
79
+ # 1. 贪心最长匹配中文业务词 (与 expand_chinese_query 同思路, 但保留原始中文词)
80
+ remaining = sentence
81
+ for cn in sorted(cn_map.keys(), key=len, reverse=True):
82
+ if cn in remaining:
83
+ primary_cn.append(cn)
84
+ # 用 expand_chinese_query 拿这个词的英文展开
85
+ for w in expand_chinese_query(cn):
86
+ en_words.add(w.lower())
87
+ remaining = remaining.replace(cn, ' ')
88
+
89
+ # 2. 英文缩写/技术词直通 (OCR 不在任何词典里也照常参与匹配)
90
+ low = sentence.lower()
91
+ for acr in _EN_ACRONYMS:
92
+ if acr in low:
93
+ en_words.add(acr)
94
+ # 也带上常见变体 (ocr → ocr; 不强行猜其它)
95
+ # 句子里其它纯英文片段 (>=3 字母) 也加入, 让索引做子串匹配
96
+ for frag in remaining.split():
97
+ frag = frag.strip()
98
+ if frag and frag.isascii() and len(frag) >= 3:
99
+ en_words.add(frag.lower())
100
+
101
+ # 3. expand_chinese_query 对整句做一次 (捕获中文分词能拆出的英文)
102
+ for w in expand_chinese_query(sentence):
103
+ en_words.add(w.lower())
104
+
105
+ # 兜底: 整句小写也作为一个匹配候选 (若全是英文/数字)
106
+ if sentence.isascii():
107
+ en_words.add(sentence.lower())
108
+
109
+ return primary_cn, sorted(en_words)
110
+
111
+
112
+ # ── 批量合并取上下文 ──────────────────────────────────────────────────
113
+
114
+ def _collect(targets, en_words):
115
+ """对预测出的所有词, 批量收集 1/3/4/5 段命中, 合并去重。
116
+
117
+ 返回 dict: {段名: 合并去重 + 排序后的列表}。
118
+ 排序: 多词命中的项排前面 (被越多词命中越相关)。
119
+ """
120
+ ki = load('code-keyword.json') or {}
121
+ si = load('ui-style.json') or {}
122
+ prds = load('prd-index.json') or {}
123
+ apis = load('code-api.json') or {}
124
+ # frontend-routes: 前端路由 + view 路径 (前端需求的主要命中源,
125
+ # code-keyword 虽有 vue 但靠 kw 键匹配, 路由页直接命中靠这里)
126
+ fr = load('frontend-routes.json') or {}
127
+
128
+ # ---- 1. 相关代码文件: 文件 -> {命中的词} ----
129
+ file_hits = {} # f -> set(命中的词)
130
+ # 1a. code-keyword: 扩展词匹配 keyword 键 + 匹配文件路径
131
+ for word in en_words:
132
+ for kw, files in ki.items():
133
+ if match_kw(word, kw):
134
+ fs = files if not targets else [f for t in targets for f in plat_filter(files, t)]
135
+ for f in fs:
136
+ file_hits.setdefault(f, set()).add(word)
137
+ # 1b. 前端 view 路径直接匹配 (前端需求不再 0 命中代码文件的根因修复:
138
+ # view 路径如 views/cost/contractSubmission/index 含 contract 就命中)
139
+ # 用「路径段包含」而非裸子串, 避免 digital-clock 被考勤(clock)误命中。
140
+ def _seg_match(word, path_lower):
141
+ """word 作为一个完整路径段命中 path (前后是 / 开头/结尾/分隔)。
142
+ 例: 'clock' 命中 'time/clock/index' (clock 是独立段), 不命中 'digital-clock'。"""
143
+ if not word or len(word) < MIN_FUZZY_LEN:
144
+ return False
145
+ # 标准化: 前后加 / 让首尾段也能匹配
146
+ norm = '/' + path_lower + '/'
147
+ # word 含 / 时 (如 time/clock) 直接子串; 否则要求前后是边界
148
+ if '/' in word:
149
+ return word in norm
150
+ return ('/' + word + '/') in norm or ('/' + word + '.') in norm or ('-' + word + '-') in norm
151
+ views = fr.get('views') or []
152
+ for vp in views:
153
+ vpl = vp.lower()
154
+ for word in en_words:
155
+ if _seg_match(word, vpl):
156
+ # 还原成 fywl-ui/... 形式 (views 里是相对 src/views 的路径)
157
+ f = 'fywl-ui/apps/ics/src/views/' + vp
158
+ file_hits.setdefault(f, set()).add(word)
159
+ # 按命中词数排序, 取前 12
160
+ files_ranked = sorted(file_hits.items(), key=lambda x: len(x[1]), reverse=True)[:12]
161
+
162
+ # ---- 3. 相关字段: field -> (count, info, {命中的词}) ----
163
+ fm = si.get('field_map') or {}
164
+ field_hits = [] # (count, field, info, 命中词数)
165
+ for field, info in fm.items():
166
+ fl = field.lower()
167
+ hit_words = [w for w in en_words if len(w) >= MIN_FUZZY_LEN and w in fl]
168
+ if hit_words:
169
+ field_hits.append((info.get('count', 0), field, info, len(hit_words)))
170
+ field_hits.sort(key=lambda x: (x[3], x[0]), reverse=True)
171
+
172
+ # ---- 4. 相关历史 PRD ----
173
+ prd_hits = []
174
+ for pf, prd in prds.items():
175
+ text = (prd.get('title', '') + ' ' + ' '.join(prd.get('keywords', []))).lower()
176
+ hit_words = [w for w in en_words if w in text]
177
+ if hit_words:
178
+ score = sum(2 for _ in hit_words)
179
+ prd_hits.append((score, pf, prd.get('title', pf)))
180
+ prd_hits.sort(reverse=True)
181
+
182
+ # ---- 5. 相关 API ----
183
+ api_hits = [(ep, f, len([w for w in en_words if len(w) >= MIN_FUZZY_LEN
184
+ and (w in ep.lower() or w in f.lower())]))
185
+ for ep, f in apis.items()
186
+ if any(len(w) >= MIN_FUZZY_LEN and (w in ep.lower() or w in f.lower())
187
+ for w in en_words)]
188
+ api_hits.sort(key=lambda x: x[2], reverse=True)
189
+
190
+ return {
191
+ 'files': files_ranked,
192
+ 'fields': field_hits,
193
+ 'prds': prd_hits,
194
+ 'apis': api_hits,
195
+ }
196
+
197
+
198
+ def _print_merged(query, platform, primary_cn, en_words, collected):
199
+ """打印合并后的 8 段 (与 context_pack 格式兼容)。"""
200
+ targets = []
201
+ if platform and platform.lower() not in ('both', '两端'):
202
+ targets = [PLATFORM_MAP.get(platform.lower(), platform)]
203
+ elif platform:
204
+ targets = sorted(set(PLATFORM_MAP.values()))
205
+
206
+ print('# Prefetch: {} (platform: {})'.format(query, platform or 'all'))
207
+ print('预测主词 (中文业务词): ' + ', '.join(primary_cn) if primary_cn else '(无精确中文业务词)')
208
+ print('批量搜索词: ' + ' '.join(en_words))
209
+ print('提示: 这是一次性批量预取, 已合并多词命中。prefetch 之后不要再串行 search/find/grep。')
210
+
211
+ # 1. 相关代码文件
212
+ files_ranked = collected['files']
213
+ print('\n## 1. 相关代码文件 ({} 个, 取前 12, 多词命中排前)'.format(len(files_ranked)))
214
+ for f, ws in files_ranked:
215
+ full = 'data/code/' + f.replace('\\', '/')
216
+ print('- {} ({}词命中: {}) -> {}'.format(f, len(ws), ','.join(sorted(ws)[:3]), full))
217
+ if not files_ranked:
218
+ print('- (无命中 — 该功能可能为全新, 或换业务词重跑 prefetch)')
219
+
220
+ # 2. 同类页面示例 (固定信息, 直接列)
221
+ si = load('ui-style.json') or {}
222
+ print('\n## 2. 同类页面示例 (风格锚点, 读 1-2 个)')
223
+ shown = 0
224
+ for proj, pdata in sorted((si.get('projects') or {}).items()):
225
+ if targets and proj not in targets:
226
+ continue
227
+ if shown >= 4:
228
+ break
229
+ examples = pdata.get('page_examples', {})
230
+ for pt, fs in sorted(examples.items()):
231
+ if pt == 'unknown':
232
+ continue
233
+ fs_files = [item['file'] if isinstance(item, dict) else item for item in fs[:2]]
234
+ print('- [{}] {}: {}'.format(proj, pt, ' | '.join(fs_files)))
235
+ shown += 1
236
+ if shown >= 4:
237
+ break
238
+
239
+ # 3. 相关字段
240
+ field_hits = collected['fields']
241
+ print('\n## 3. 相关字段 (真实字段名->中文标题, 多词命中排前)')
242
+ for cnt, field, info, n in field_hits[:10]:
243
+ print('- {} -> {} ({}x, {}词命中)'.format(field, '/'.join(info.get('titles', [])[:3]), cnt, n))
244
+ if not field_hits:
245
+ print('- (无直接相关字段)')
246
+
247
+ # 4. 相关历史 PRD
248
+ prd_hits = collected['prds']
249
+ print('\n## 4. 相关历史 PRD (data/docs/prd/)')
250
+ for s, pf, title in prd_hits[:5]:
251
+ print('- {} - {}'.format(pf, title[:60]))
252
+ if not prd_hits:
253
+ print('- (无相关 PRD)')
254
+
255
+ # 5. 相关 API
256
+ api_hits = collected['apis']
257
+ print('\n## 5. 相关 API (取前 8 / 共 {}, 多词命中排前)'.format(len(api_hits)))
258
+ for ep, f, n in sorted(api_hits, key=lambda x: x[2], reverse=True)[:8]:
259
+ print('- {} -> {} ({}词命中)'.format(ep, f.split('/')[-1], n))
260
+ if not api_hits:
261
+ print('- (无相关 API)')
262
+
263
+ # 6. 风格/图标/模板速查 (固定)
264
+ print('\n## 6. 风格真源速查')
265
+ print('- Web Token: data/index/ref-vben-style.json (primary=hsl(214.58 86.34% 59.8%))')
266
+ print('- 图标 (禁 emoji): data/index/ref-icon.json (Web=antd 内联SVG, APP=vant 字体)')
267
+ print('- 模板: .qoder/templates/prototype-web.html / prototype-app.html')
268
+ print('- PRD 模板: .qoder/templates/prd-full-template.md / prd-quick-template.md')
269
+
270
+ # 7. Repo Wiki
271
+ code_dir = os.path.join(BASE, 'data', 'code')
272
+ wiki_results = []
273
+ try:
274
+ from common.repowiki import search_wiki, build_wiki_index
275
+ wiki_idx_path = os.path.join(BUILD_CACHE_DIR, 'wiki-index.json')
276
+ if not os.path.isfile(wiki_idx_path):
277
+ build_wiki_index(code_dir, wiki_idx_path)
278
+ cn_map = get_cn_map_with_auto()
279
+ # Wiki 用主词 + 整句各查一次, 合并 (Wiki 命中往往最精准)
280
+ wiki_q = primary_cn[0] if primary_cn else query
281
+ wiki_results = search_wiki(wiki_q, INDEX_DIR, code_dir, cn_map)
282
+ except Exception:
283
+ pass
284
+ if wiki_results:
285
+ print('\n## 7. Repo Wiki (语义级模块文档, 比源码更精准)')
286
+ print(' 相关模块文档 ({} 条, 带行号引用, 优先读这些):'.format(len(wiki_results)))
287
+ for i, wr in enumerate(wiki_results[:5], 1):
288
+ print(' {}. [{}] {}'.format(i, wr.get('project', '?'), wr.get('title', '')))
289
+ if wr.get('dependent_files'):
290
+ print(' 核心文件: ' + ', '.join(wr['dependent_files'][:2]))
291
+ if wr.get('md_path'):
292
+ print(' 文档: ' + os.path.relpath(wr['md_path'], BASE))
293
+
294
+ # 8. 数据库表结构 (主词查一次即可)
295
+ try:
296
+ from search_index import _get_kg_db
297
+ con = _get_kg_db()
298
+ if con is not None:
299
+ tbl_q = primary_cn[0] if primary_cn else query
300
+ kw_lower = tbl_q.lower()
301
+ import re as _re
302
+ snake = _re.sub(r'([a-z0-9])([A-Z])', r'\1_\2', tbl_q).lower()
303
+ tbl_hits = con.execute(
304
+ "SELECT DISTINCT table_name, db FROM db_tables "
305
+ "WHERE lower(table_name) LIKE ? OR lower(table_name) LIKE ? LIMIT 3",
306
+ ['%' + kw_lower + '%', '%' + snake + '%']
307
+ ).fetchall()
308
+ if tbl_hits:
309
+ print('\n## 8. 数据库表结构 (kg_db.duckdb, QAS测试环境)')
310
+ for tn, dbn in tbl_hits[:3]:
311
+ cols = con.execute(
312
+ "SELECT column_name, data_type FROM db_columns WHERE table_name = ? LIMIT 6",
313
+ [tn]
314
+ ).fetchall()
315
+ print(' [{}.{}] {}'.format(dbn, tn, len(cols)))
316
+ for cn, dt in cols:
317
+ print(' {} ({})'.format(cn, dt))
318
+ except Exception:
319
+ pass # kg_db 不存在 → 跳过
320
+
321
+
322
+ # ── 入口 ──────────────────────────────────────────────────────────
323
+
324
+ def main():
325
+ args = sys.argv[1:]
326
+ platform = None
327
+ if '--platform' in args:
328
+ i = args.index('--platform')
329
+ platform = args[i + 1] if i + 1 < len(args) else None
330
+ args = args[:i] + args[i + 2:]
331
+ if not args:
332
+ print(__doc__)
333
+ return 1
334
+ query = args[0]
335
+
336
+ # 词预测
337
+ primary_cn, en_words = predict_words(query)
338
+
339
+ # 降级: 预测不出任何英文词 → 回退单词 context_pack (保持兼容)
340
+ if not en_words:
341
+ import context_pack as _cp
342
+ # 直接调 context_pack 的计算逻辑 (它会自己 print)
343
+ _cp._compute_and_print(query, platform, None)
344
+ return 0
345
+
346
+ targets = []
347
+ if platform and platform.lower() not in ('both', '两端'):
348
+ targets = [PLATFORM_MAP.get(platform.lower(), platform)]
349
+ elif platform:
350
+ targets = sorted(set(PLATFORM_MAP.values()))
351
+
352
+ # 批量合并取上下文
353
+ collected = _collect(targets, en_words)
354
+ _print_merged(query, platform, primary_cn, en_words, collected)
355
+ return 0
356
+
357
+
358
+ if __name__ == '__main__':
359
+ sys.exit(main())
@@ -1,5 +1,11 @@
1
1
  #!/usr/bin/env python3
2
2
  # -*- coding: utf-8 -*-
3
+ # v3.0 路径自举: 引导到 common/bootstrap, 统一 sys.path 逻辑
4
+ import os as _o, sys as _s
5
+ _cp = _o.path.join(_o.path.dirname(_o.path.dirname(_o.path.abspath(__file__))), 'common')
6
+ if _cp not in _s.path: _s.path.insert(0, _cp)
7
+ from bootstrap import setup; setup()
8
+
3
9
  """
4
10
  Search knowledge index with platform filtering and PRD search.
5
11
 
@@ -82,6 +88,27 @@ def _get_duckdb():
82
88
  return None
83
89
 
84
90
 
91
+ # 第二个 DuckDB (kg_db.duckdb, 数据库图谱) 的惰性连接缓存。
92
+ # context_pack 第 8 段每次都新开连接读这个库, 与上面的 kg.duckdb 单例不对称,
93
+ # 每次调用多一次连接开销。这里镜像 _get_duckdb 的单例模式, 供 context_pack 复用。
94
+ _kgdb_con = None
95
+
96
+
97
+ def _get_kg_db():
98
+ global _kgdb_con
99
+ if _kgdb_con is not None:
100
+ return _kgdb_con
101
+ db_path = os.path.join(INDEX_DIR, 'kg_db.duckdb')
102
+ if not os.path.isfile(db_path):
103
+ return None
104
+ try:
105
+ import duckdb
106
+ _kgdb_con = duckdb.connect(db_path, read_only=True)
107
+ return _kgdb_con
108
+ except Exception:
109
+ return None
110
+
111
+
85
112
  def _load_keyword_with_cache(con):
86
113
  """加载 code-keyword (11836 行), 带 pickle 缓存层。
87
114
 
@@ -227,11 +254,24 @@ def _match_keyword(query_word, kw):
227
254
 
228
255
 
229
256
  def _search_cache_key(query, platform):
230
- """缓存 key: query|platform|keyword-index mtime。"""
257
+ """缓存 key: query|platform|数据源 mtime 指纹。
258
+
259
+ 数据真正存在 DuckDB(kg.duckdb), code-keyword.json 在 DuckDB 模式下不存在。
260
+ 若只用 JSON 的 mtime 做指纹, DuckDB 模式下恒为 '0' —— DuckDB 增量更新后,
261
+ 旧的搜索缓存仍会命中, 返回陈旧结果(与 context_pack 同源 bug, 那边已修)。
262
+ 所以指纹必须以 kg.duckdb mtime 为主, JSON 为辅。
263
+ """
231
264
  import hashlib
232
- ki_path = os.path.join(BASE, 'data', 'index', 'code-keyword.json') if os.path.isdir(os.path.join(BASE, 'data', 'index')) else None
233
- mtime = '%d' % os.path.getmtime(ki_path) if ki_path and os.path.isfile(ki_path) else '0'
234
- raw = '{}|{}|{}'.format(query.lower(), platform or '', mtime)
265
+ mtime_str = ''
266
+ # DuckDB (数据真正来源, 必须算)
267
+ duckdb_path = os.path.join(BASE, 'data', 'index', 'kg.duckdb')
268
+ if os.path.isfile(duckdb_path):
269
+ mtime_str += 'duck:%d_' % os.path.getmtime(duckdb_path)
270
+ # ② JSON 索引 (DuckDB 缺失时的降级数据源, 一起算)
271
+ ki_path = os.path.join(BASE, 'data', 'index', 'code-keyword.json')
272
+ if os.path.isfile(ki_path):
273
+ mtime_str += 'kw:%d' % os.path.getmtime(ki_path)
274
+ raw = '{}|{}|{}'.format(query.lower(), platform or '', mtime_str or '0')
235
275
  return hashlib.md5(raw.encode('utf-8')).hexdigest()[:16]
236
276
 
237
277
 
@@ -308,10 +348,9 @@ def _check_index_freshness():
308
348
 
309
349
 
310
350
  def search_keywords(query, platform=None):
311
- # 新鲜度检查 (代码改了但索引没更新 提示, 不阻塞)
312
- _check_index_freshness()
313
-
314
- # 结果缓存: 同 query+platform+索引未变 → 直接返回缓存输出
351
+ # 先查缓存 (命中即数据未变, 直接还)。缓存 key 已含 kg.duckdb mtime, 命中就说明
352
+ # 数据源没变, 跳过 _check_index_freshness 的 os.walk —— 50 人团队每天几十次
353
+ # 搜索, 每次都 walk 目录是纯浪费。未命中才做新鲜度检查 + 计算。
315
354
  cache_key = _search_cache_key(query, platform)
316
355
  cache_path = os.path.join(BASE, '.qoder', '.runtime', 'search-cache-%s.txt' % cache_key)
317
356
  if os.path.isfile(cache_path):
@@ -324,6 +363,9 @@ def search_keywords(query, platform=None):
324
363
  except OSError:
325
364
  pass
326
365
 
366
+ # 缓存未命中 → 才做新鲜度检查 (代码改了但索引没更新 → 提示, 不阻塞)
367
+ _check_index_freshness()
368
+
327
369
  # 缓存未命中, 正常计算并写入缓存
328
370
  import io
329
371
  old_stdout = sys.stdout
@@ -580,20 +622,33 @@ def show_top_keywords():
580
622
 
581
623
 
582
624
  def show_components():
583
- meta = load_index('ui-style-meta.json', hint='Run: python build_style_index.py')
584
- if meta is None:
625
+ # 组件统计存在 DuckDB style_meta.common_components/form_components (JSON ),
626
+ # kg_build -> kg_duckdb 从 ui-style.json 导入。
627
+ # 注意: 不是独立 JSON 文件, 走 DuckDB 路径。
628
+ si = load_index('ui-style.json')
629
+ if si is None:
630
+ print('UI 样式索引未构建。组件统计在 ui-style.json → kg.duckdb.style_meta 表。')
631
+ print('构建: npx @hupan56/wlkj run kg_build.py --rebuild (管理员)')
632
+ print(' 非 admin: npx @hupan56/wlkj doctor (拉团队已构建的 kg.duckdb)')
633
+ return
634
+ common = si.get('common_components', {})
635
+ forms = si.get('form_components', {})
636
+ if not common and not forms:
637
+ print('style_meta 表的组件列为空 — kg_duckdb 导入时 ui-style.json 缺 common_components。')
638
+ print('重建: npx @hupan56/wlkj run build_style_index.py 然后 kg_build.py --rebuild')
585
639
  return
586
640
  print('=== UI Components Usage ===')
587
641
  print()
588
642
  print('Ant Design Vue (import counts):')
589
- for comp, count in meta.get('common_components', {}).items():
643
+ for comp, count in common.items():
590
644
  print(' {} {}'.format(comp.ljust(20), str(count).ljust(5)))
591
645
  print()
592
646
  print('Form Components (usage in schemas):')
593
- for comp, count in sorted(meta.get('form_components', {}).items(), key=lambda x: x[1], reverse=True)[:20]:
647
+ for comp, count in sorted(forms.items(), key=lambda x: x[1], reverse=True)[:20]:
594
648
  print(' {} {}'.format(comp.ljust(20), str(count).ljust(5)))
595
649
 
596
650
 
651
+
597
652
  def show_vben():
598
653
  data = load_index('ref-vben-style.json')
599
654
  if data is None:
@@ -754,8 +809,9 @@ def _graph_cli(method, *args):
754
809
  """
755
810
  try:
756
811
  if method == 'get_workflow':
757
- # DuckDB 路径
758
- import kg_duckdb as K
812
+ # DuckDB 路径 (v3.0: kg_duckdb 已迁入 kg/ 子包)
813
+ from kg.kg_duckdb import get_db as _kg_get_db
814
+ import kg.kg_duckdb as K
759
815
  q = (args[0] if args else '').strip()
760
816
  if not q:
761
817
  print('请提供模块名, 如: assets / cost / hr / maintenance')
@@ -0,0 +1,11 @@
1
+ # 子包初始化: 注入 scripts/根 + 所有子包到 sys.path
2
+ # 让跨包 import (旧式不带前缀) 仍可用, 兼容现有代码
3
+ import os as _os, sys as _sys
4
+ _root = _os.path.dirname(_os.path.dirname(_os.path.abspath(__file__)))
5
+ if _root not in _sys.path:
6
+ _sys.path.insert(0, _root)
7
+ # 注入所有子包目录 (兼容旧式 import xxx)
8
+ for _sub in ('kg', 'build', 'test', 'setup', 'io'):
9
+ _subdir = _os.path.join(_root, _sub)
10
+ if _os.path.isdir(_subdir) and _subdir not in _sys.path:
11
+ _sys.path.insert(0, _subdir)