@hupan56/wlkj 2.2.6 → 2.3.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hupan56/wlkj",
3
- "version": "2.2.6",
3
+ "version": "2.3.1",
4
4
  "description": "AI Product R&D Workflow - PRD/Prototype/Search/Task/Report",
5
5
  "bin": {
6
6
  "wlkj": "bin/cli.js"
@@ -0,0 +1,300 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ repowiki.py - 读取 Qoder IDE 生成的 Repo Wiki, 转成搜索可用的索引。
4
+
5
+ Repo Wiki 是 Qoder IDE 用 LLM 对整个仓库生成的语义级知识图谱:
6
+ - wiki_catalogs: 模块/功能文档 (含 dependent_files 源文件映射)
7
+ - wiki_items: 文档标题/描述
8
+ - knowledge_relations: 模块间父子/依赖关系
9
+ - content/*.md: 带行号引用的人类可读文档
10
+
11
+ 位置: data/code/{project}/.qoder/repowiki/{lang}/meta/repowiki-metadata.json
12
+ data/code/{project}/.qoder/repowiki/{lang}/content/{分类}/{页面}.md
13
+
14
+ 本模块把这些转成:
15
+ - wiki-index.json: {关键词: [{project, title, catalog_id, md_path, dependent_files}]}
16
+ - 供 search_index --wiki 和 context_pack 使用
17
+ """
18
+
19
+ import json
20
+ import os
21
+ import re
22
+ from pathlib import Path
23
+ from typing import Dict, List, Optional
24
+
25
+
26
+ def find_repowiki_dirs(code_dir: str) -> List[tuple]:
27
+ """扫描 data/code/{project}/.qoder/repowiki/, 返回 [(project, wiki_root), ...]。
28
+
29
+ 健壮性: repowiki 目录可能存在但为空/损坏/正在生成中。
30
+ 只返回 metadata 文件存在且非空的目录。
31
+ """
32
+ results = []
33
+ if not os.path.isdir(code_dir):
34
+ return results
35
+ try:
36
+ projects = os.listdir(code_dir)
37
+ except OSError:
38
+ return results
39
+ for proj in projects:
40
+ try:
41
+ wiki_base = os.path.join(code_dir, proj, '.qoder', 'repowiki')
42
+ if not os.path.isdir(wiki_base):
43
+ continue
44
+ for lang in os.listdir(wiki_base):
45
+ lang_dir = os.path.join(wiki_base, lang)
46
+ if not os.path.isdir(lang_dir):
47
+ continue
48
+ meta = os.path.join(lang_dir, 'meta', 'repowiki-metadata.json')
49
+ if not os.path.isfile(meta):
50
+ continue
51
+ # 校验文件非空 (生成中可能写出 0 字节文件)
52
+ try:
53
+ if os.path.getsize(meta) < 10:
54
+ continue # 太小, 可能正在生成
55
+ except OSError:
56
+ continue
57
+ results.append((proj, lang_dir))
58
+ break # 每个项目只取一个语言版本
59
+ except OSError:
60
+ continue # 权限/路径问题, 跳过这个项目
61
+ return results
62
+
63
+
64
+ def load_wiki_metadata(wiki_root: str) -> Optional[dict]:
65
+ """加载 repowiki-metadata.json。
66
+
67
+ 健壮性: 文件可能不存在/空/损坏/正在生成。
68
+ """
69
+ meta_path = os.path.join(wiki_root, 'meta', 'repowiki-metadata.json')
70
+ try:
71
+ if not os.path.isfile(meta_path):
72
+ return None
73
+ if os.path.getsize(meta_path) < 10:
74
+ return None # 正在生成或空
75
+ with open(meta_path, encoding='utf-8') as f:
76
+ data = json.load(f)
77
+ # 校验基本结构
78
+ if not isinstance(data, dict):
79
+ return None
80
+ # wiki_catalogs 可能为空 (生成中)
81
+ if not data.get('wiki_catalogs'):
82
+ return None # 没有内容, 视为无效
83
+ return data
84
+ except (OSError, json.JSONDecodeError, UnicodeDecodeError):
85
+ return None
86
+
87
+
88
+ def _extract_keywords(text: str) -> List[str]:
89
+ """从标题/描述提取搜索关键词 (中文分词 + 英文小写)。"""
90
+ if not text:
91
+ return []
92
+ words = set()
93
+ # 英文: 按非字母数字分割, 保留长度>=3的
94
+ for tok in re.split(r'[^a-zA-Z0-9]+', text):
95
+ tok = tok.strip().lower()
96
+ if len(tok) >= 3:
97
+ words.add(tok)
98
+ # 中文: 整段作为关键词 (让 CN_MAP 在搜索时做分词扩展)
99
+ # 提取连续中文片段
100
+ for m in re.finditer(r'[\u4e00-\u9fff]{2,}', text):
101
+ words.add(m.group())
102
+ return list(words)
103
+
104
+
105
+ def build_wiki_index(code_dir: str, output_path: str) -> dict:
106
+ """扫描所有项目的 Repo Wiki, 构建 wiki-index.json。
107
+
108
+ Returns:
109
+ {'index': {...}, 'stats': {...}} 或 {'index': {}, 'stats': {...}}
110
+ 始终返回含 'index' 和 'stats' 两个 key 的 dict (调用方可安全 .get)。
111
+ wiki 不存在/为空/损坏时 index 为 {}, stats 计数为 0。
112
+ """
113
+ index = {} # keyword -> [entry, ...]
114
+ stats = {'projects': 0, 'pages': 0, 'keywords': 0}
115
+
116
+ wiki_dirs = find_repowiki_dirs(code_dir)
117
+ if not wiki_dirs:
118
+ # wiki 不存在或都损坏 → 写空索引, 避免每次搜索都重建
119
+ _write_wiki_index(output_path, index, stats)
120
+ return {'index': index, 'stats': stats}
121
+
122
+ for project, wiki_root in wiki_dirs:
123
+ try:
124
+ meta = load_wiki_metadata(wiki_root)
125
+ if not meta:
126
+ continue # 这个项目的 wiki 损坏/为空, 跳过
127
+ stats['projects'] += 1
128
+
129
+ catalogs = meta.get('wiki_catalogs') or []
130
+ items = meta.get('wiki_items') or []
131
+ # catalog_id -> title 映射
132
+ id_to_title = {}
133
+ for it in items:
134
+ if isinstance(it, dict):
135
+ cid = it.get('catalog_id')
136
+ if cid:
137
+ id_to_title[cid] = it.get('title', '')
138
+
139
+ for cat in catalogs:
140
+ try:
141
+ if not isinstance(cat, dict):
142
+ continue
143
+ cat_id = cat.get('id', '')
144
+ name = cat.get('name', '') or ''
145
+ desc = cat.get('description', '') or ''
146
+ dep_files = cat.get('dependent_files', '') or ''
147
+ title = id_to_title.get(cat_id, name)
148
+
149
+ # 找对应的 markdown 文档 (可能不存在, 生成中)
150
+ md_path = _find_md_for_catalog(wiki_root, cat_id, name)
151
+ if not md_path:
152
+ continue # 文档还没生成, 跳过
153
+
154
+ stats['pages'] += 1
155
+
156
+ # 提取关键词
157
+ kw_text = ' '.join([str(title), str(name), str(desc)])
158
+ if dep_files:
159
+ kw_text += ' ' + str(dep_files).replace(',', ' ').replace('/', ' ')
160
+ keywords = _extract_keywords(kw_text)
161
+
162
+ entry = {
163
+ 'project': project,
164
+ 'title': str(title or name),
165
+ 'catalog_id': str(cat_id),
166
+ 'md_path': md_path,
167
+ 'dependent_files': [f.strip() for f in str(dep_files).split(',') if f.strip()],
168
+ 'description': str(desc),
169
+ }
170
+
171
+ for kw in keywords:
172
+ index.setdefault(kw, [])
173
+ if entry not in index[kw]:
174
+ index[kw].append(entry)
175
+ except (OSError, TypeError, ValueError):
176
+ continue # 单条 catalog 出错不影响其他
177
+ except (OSError, TypeError):
178
+ continue # 整个项目的 wiki 出错, 跳过
179
+
180
+ stats['keywords'] = len(index)
181
+ _write_wiki_index(output_path, index, stats)
182
+ return {'index': index, 'stats': stats}
183
+
184
+
185
+ def _write_wiki_index(output_path: str, index: dict, stats: dict) -> None:
186
+ """安全写入 wiki-index.json (best-effort, 失败不崩)。"""
187
+ try:
188
+ os.makedirs(os.path.dirname(output_path), exist_ok=True)
189
+ with open(output_path, 'w', encoding='utf-8') as f:
190
+ json.dump({'index': index, 'stats': stats}, f, ensure_ascii=False, indent=2)
191
+ except OSError:
192
+ pass
193
+
194
+
195
+ def _find_md_for_catalog(wiki_root: str, catalog_id: str, name: str) -> Optional[str]:
196
+ """在 content/ 目录下找对应的 markdown 文件。
197
+
198
+ 文件名通常等于 catalog name (.md), 在某个子分类目录下。
199
+ 健壮性: name 为空/content 目录不存在时返回 None。
200
+ """
201
+ if not name:
202
+ return None
203
+ content_dir = os.path.join(wiki_root, 'content')
204
+ if not os.path.isdir(content_dir):
205
+ return None
206
+ target = name + '.md'
207
+ for root, dirs, files in os.walk(content_dir):
208
+ if target in files:
209
+ return os.path.join(root, target)
210
+ # 模糊匹配: name 的第一段
211
+ short = name.split('-')[0].split('(')[0].strip()
212
+ if short and short != name:
213
+ target2 = short + '.md'
214
+ for root, dirs, files in os.walk(content_dir):
215
+ for fn in files:
216
+ if fn == target2 or short in fn:
217
+ return os.path.join(root, fn)
218
+ return None
219
+
220
+
221
+ def load_wiki_index(index_dir: str) -> dict:
222
+ """加载已构建的 wiki-index.json。返回 {} 表示不存在。"""
223
+ path = os.path.join(index_dir, 'wiki-index.json')
224
+ try:
225
+ with open(path, encoding='utf-8') as f:
226
+ data = json.load(f)
227
+ return data.get('index', {}) if isinstance(data, dict) else data
228
+ except (OSError, json.JSONDecodeError):
229
+ return {}
230
+
231
+
232
+ def search_wiki(query: str, index_dir: str, code_dir: str,
233
+ cn_map: Optional[dict] = None) -> List[dict]:
234
+ """在 Repo Wiki 索引里搜索。
235
+
236
+ 自动扩展中文查询 (用 cn_map), 返回匹配的 wiki 条目列表。
237
+ 如果 wiki-index.json 不存在, 自动构建。
238
+ """
239
+ wiki_idx = load_wiki_index(index_dir)
240
+ if not wiki_idx:
241
+ # 自动构建
242
+ out = os.path.join(index_dir, 'wiki-index.json')
243
+ result = build_wiki_index(code_dir, out)
244
+ if isinstance(result, dict):
245
+ wiki_idx = result.get('index', {})
246
+ else:
247
+ wiki_idx = result
248
+
249
+ if not wiki_idx:
250
+ return []
251
+
252
+ # 扩展查询词 (复用 search_index 的 CN_MAP 分词逻辑)
253
+ words = set()
254
+ q_lower = query.lower()
255
+ words.add(q_lower)
256
+ # 中文分词: 用 cn_map 拆
257
+ if cn_map:
258
+ remaining = query
259
+ for cn in sorted(cn_map.keys(), key=len, reverse=True):
260
+ if cn in remaining:
261
+ for en in cn_map[cn].split():
262
+ words.add(en.lower())
263
+ remaining = remaining.replace(cn, ' ')
264
+ # 剩余英文片段
265
+ for frag in remaining.split():
266
+ if frag and frag.isascii():
267
+ words.add(frag.lower())
268
+ # 中文整段也加
269
+ for m in re.finditer(r'[\u4e00-\u9fff]{2,}', query):
270
+ words.add(m.group())
271
+
272
+ # 搜索: 子串匹配, 标题命中优先 (TF 排序)
273
+ scored = [] # (score, entry)
274
+ seen_titles = set()
275
+ for kw, entries in wiki_idx.items():
276
+ kw_lower = kw.lower()
277
+ for word in words:
278
+ if word in kw_lower or kw_lower in word:
279
+ for entry in entries:
280
+ key = (entry.get('project'), entry.get('title'))
281
+ if key in seen_titles:
282
+ continue
283
+ # 打分: 标题命中 = 10分, 描述命中 = 5分, 依赖文件命中 = 1分
284
+ title = (entry.get('title') or '').lower()
285
+ desc = (entry.get('description') or '').lower()
286
+ score = 0
287
+ if word in title or title in word:
288
+ score = 10
289
+ elif word in desc:
290
+ score = 5
291
+ else:
292
+ score = 1 # 依赖文件命中
293
+ if score > 0:
294
+ seen_titles.add(key)
295
+ scored.append((score, entry))
296
+ break
297
+
298
+ # 按分数降序
299
+ scored.sort(key=lambda x: x[0], reverse=True)
300
+ return [entry for _, entry in scored[:15]]
@@ -247,19 +247,43 @@ def _compute_and_print(query, platform, page_type):
247
247
  print('- 模板: .qoder/templates/prototype-web.html / prototype-app.html')
248
248
  print('- PRD 模板: .qoder/templates/prd-full-template.md / prd-quick-template.md')
249
249
 
250
- # ---- 7. Repo Wiki ----
251
- wikis = []
252
- if os.path.isdir(os.path.join(BASE, '.qoder', 'repowiki')):
253
- wikis.append('.qoder/repowiki/')
250
+ # ---- 7. Repo Wiki (语义级模块文档, 优先参考) ----
254
251
  code_dir = os.path.join(BASE, 'data', 'code')
252
+ wiki_results = []
253
+ try:
254
+ from common.repowiki import search_wiki, build_wiki_index
255
+ from common.terms import get_cn_map_with_auto
256
+ wiki_idx_path = os.path.join(INDEX_DIR, 'wiki-index.json')
257
+ if not os.path.isfile(wiki_idx_path):
258
+ build_wiki_index(code_dir, wiki_idx_path)
259
+ cn_map = get_cn_map_with_auto()
260
+ wiki_results = search_wiki(query, INDEX_DIR, code_dir, cn_map)
261
+ except Exception:
262
+ pass
263
+
264
+ # 也列出 wiki 目录 (供手动浏览)
265
+ wiki_dirs = []
266
+ if os.path.isdir(os.path.join(BASE, '.qoder', 'repowiki')):
267
+ wiki_dirs.append('.qoder/repowiki/')
255
268
  if os.path.isdir(code_dir):
256
269
  for p in sorted(os.listdir(code_dir)):
257
270
  if os.path.isdir(os.path.join(code_dir, p, '.qoder', 'repowiki')):
258
- wikis.append('data/code/{}/.qoder/repowiki/'.format(p))
259
- if wikis:
260
- print('\n## 7. Repo Wiki 可用 (模块级文档, 优先参考)')
261
- for w in wikis:
262
- print('- ' + w)
271
+ wiki_dirs.append('data/code/{}/.qoder/repowiki/'.format(p))
272
+
273
+ if wiki_results or wiki_dirs:
274
+ print('\n## 7. Repo Wiki (语义级模块文档, 比源码更精准)')
275
+ if wiki_results:
276
+ print(' 相关模块文档 ({} 条, 带行号引用, 优先读这些):'.format(len(wiki_results)))
277
+ for i, wr in enumerate(wiki_results[:5], 1):
278
+ print(' {}. [{}] {}'.format(i, wr.get('project','?'), wr.get('title','')))
279
+ if wr.get('dependent_files'):
280
+ print(' 核心文件: ' + ', '.join(wr['dependent_files'][:2]))
281
+ if wr.get('md_path'):
282
+ print(' 文档: ' + os.path.relpath(wr['md_path'], BASE))
283
+ elif wiki_dirs:
284
+ print(' Wiki 可用但无精确匹配, 手动浏览:')
285
+ for w in wiki_dirs:
286
+ print(' - ' + w)
263
287
 
264
288
 
265
289
  if __name__ == '__main__':
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env python3
1
+ #!/usr/bin/env python3
2
2
  # -*- coding: utf-8 -*-
3
3
  """
4
4
  Weekly Knowledge Graph Update
@@ -1053,6 +1053,25 @@ def main():
1053
1053
  if not (sync_only or prd_only):
1054
1054
  verify_result = verify_indexes(config)
1055
1055
 
1056
+ # Step 5.6: Build Repo Wiki index (Qoder IDE 生成的语义级模块文档)
1057
+ if not (sync_only or prd_only):
1058
+ try:
1059
+ from common.repowiki import build_wiki_index, find_repowiki_dirs
1060
+ code_dir = os.path.join(BASE, 'data', 'code')
1061
+ wiki_dirs = find_repowiki_dirs(code_dir)
1062
+ if wiki_dirs:
1063
+ print('\n=== Building Wiki Index (Repo Wiki 语义图谱) ===')
1064
+ out = os.path.join(INDEX_DIR, 'wiki-index.json')
1065
+ r = build_wiki_index(code_dir, out)
1066
+ if isinstance(r, dict) and r.get('stats'):
1067
+ s = r['stats']
1068
+ print(' Wiki: {} 项目 / {} 模块文档 / {} 关键词'.format(
1069
+ s.get('projects',0), s.get('pages',0), s.get('keywords',0)))
1070
+ else:
1071
+ print('\n=== Wiki Index: 无 Repo Wiki (可选, Qoder IDE 生成后自动纳入) ===')
1072
+ except Exception as e:
1073
+ print(' [WARN] Wiki index build failed (不阻塞): {}'.format(str(e)[:80]))
1074
+
1056
1075
  # Step 6: Write meta (读 module-map 拿真实 project 数; counts 用已加载的 ki)
1057
1076
  module_map = load_json(os.path.join(INDEX_DIR, 'module-map.json'))
1058
1077
  if shared_ki is None:
@@ -464,6 +464,52 @@ def show_vben():
464
464
  print()
465
465
 
466
466
 
467
+ def search_wiki_cli(query):
468
+ """搜 Repo Wiki (Qoder IDE 生成的语义级模块文档)。"""
469
+ try:
470
+ from common.repowiki import search_wiki, build_wiki_index
471
+ from common.terms import get_cn_map_with_auto
472
+ except ImportError:
473
+ print('Wiki 搜索不可用 (common.repowiki 模块缺失)')
474
+ return
475
+
476
+ cn_map = get_cn_map_with_auto()
477
+ code_dir = os.path.join(BASE, 'data', 'code')
478
+ index_dir = os.path.join(BASE, 'data', 'index')
479
+
480
+ # 自动构建 wiki 索引 (若不存在)
481
+ wiki_idx_path = os.path.join(index_dir, 'wiki-index.json')
482
+ if not os.path.isfile(wiki_idx_path):
483
+ print('[构建] 首次使用, 正在从 Repo Wiki 构建语义索引...')
484
+ r = build_wiki_index(code_dir, wiki_idx_path)
485
+ if isinstance(r, dict) and r.get('stats'):
486
+ s = r['stats']
487
+ print(' 完成: {} 项目 / {} 模块文档 / {} 关键词\n'.format(
488
+ s.get('projects',0), s.get('pages',0), s.get('keywords',0)))
489
+ else:
490
+ print(' 未找到 Repo Wiki (data/code/*/.qoder/repowiki/)')
491
+ print(' 在 Qoder IDE 里对项目生成 Repo Wiki 后可用')
492
+ return
493
+
494
+ results = search_wiki(query, index_dir, code_dir, cn_map)
495
+ if not results:
496
+ print('No wiki pages match: {}'.format(query))
497
+ return
498
+
499
+ print('Wiki pages matching: {} ({} found)\n'.format(query, len(results)))
500
+ for i, r in enumerate(results, 1):
501
+ print(' {}. [{}] {}'.format(i, r.get('project','?'), r.get('title','')))
502
+ if r.get('dependent_files'):
503
+ deps = r['dependent_files'][:3]
504
+ print(' 依赖: ' + ', '.join(deps[:2]))
505
+ if len(r['dependent_files']) > 2:
506
+ print(' (共 {} 个文件)'.format(len(r['dependent_files'])))
507
+ if r.get('md_path'):
508
+ rel = os.path.relpath(r['md_path'], BASE)
509
+ print(' 文档: ' + rel)
510
+ print('\n 提示: 读 wiki 文档 (带行号引用) 比读源码更省 token')
511
+
512
+
467
513
  def print_usage():
468
514
  print('Usage:')
469
515
  print(' search_index.py <keyword> [--platform web|app] - Search code')
@@ -472,6 +518,7 @@ def print_usage():
472
518
  print(' search_index.py --field <name> - Field usage')
473
519
  print(' search_index.py --api <keyword> - API endpoints')
474
520
  print(' search_index.py --components - UI components')
521
+ print(' search_index.py --wiki <keyword> - Repo Wiki 语义模块文档 (精准)')
475
522
  print(' search_index.py --modules - Project overview')
476
523
  print(' search_index.py --list - Top 50 keywords')
477
524
  print(' search_index.py --vben - Vben design tokens')
@@ -500,6 +547,8 @@ if __name__ == '__main__':
500
547
  search_field(args[1])
501
548
  elif args[0] == '--api' and len(args) >= 2:
502
549
  search_api(args[1])
550
+ elif args[0] == '--wiki' and len(args) >= 2:
551
+ search_wiki_cli(args[1])
503
552
  elif args[0] == '--components':
504
553
  show_components()
505
554
  elif args[0] == '--modules':