@hupan56/wlkj 2.3.0 → 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.3.0",
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"
@@ -24,31 +24,64 @@ from typing import Dict, List, Optional
24
24
 
25
25
 
26
26
  def find_repowiki_dirs(code_dir: str) -> List[tuple]:
27
- """扫描 data/code/{project}/.qoder/repowiki/, 返回 [(project, wiki_root), ...]。"""
27
+ """扫描 data/code/{project}/.qoder/repowiki/, 返回 [(project, wiki_root), ...]。
28
+
29
+ 健壮性: repowiki 目录可能存在但为空/损坏/正在生成中。
30
+ 只返回 metadata 文件存在且非空的目录。
31
+ """
28
32
  results = []
29
33
  if not os.path.isdir(code_dir):
30
34
  return results
31
- for proj in os.listdir(code_dir):
32
- # repowiki 目录 (可能是 zh/ 或 en/)
33
- wiki_base = os.path.join(code_dir, proj, '.qoder', 'repowiki')
34
- if not os.path.isdir(wiki_base):
35
- continue
36
- for lang in os.listdir(wiki_base):
37
- lang_dir = os.path.join(wiki_base, lang)
38
- meta = os.path.join(lang_dir, 'meta', 'repowiki-metadata.json')
39
- if os.path.isfile(meta):
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
40
57
  results.append((proj, lang_dir))
41
58
  break # 每个项目只取一个语言版本
59
+ except OSError:
60
+ continue # 权限/路径问题, 跳过这个项目
42
61
  return results
43
62
 
44
63
 
45
64
  def load_wiki_metadata(wiki_root: str) -> Optional[dict]:
46
- """加载 repowiki-metadata.json。"""
65
+ """加载 repowiki-metadata.json。
66
+
67
+ 健壮性: 文件可能不存在/空/损坏/正在生成。
68
+ """
47
69
  meta_path = os.path.join(wiki_root, 'meta', 'repowiki-metadata.json')
48
70
  try:
71
+ if not os.path.isfile(meta_path):
72
+ return None
73
+ if os.path.getsize(meta_path) < 10:
74
+ return None # 正在生成或空
49
75
  with open(meta_path, encoding='utf-8') as f:
50
- return json.load(f)
51
- except (OSError, json.JSONDecodeError):
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):
52
85
  return None
53
86
 
54
87
 
@@ -73,76 +106,100 @@ def build_wiki_index(code_dir: str, output_path: str) -> dict:
73
106
  """扫描所有项目的 Repo Wiki, 构建 wiki-index.json。
74
107
 
75
108
  Returns:
76
- 构建的索引 dict: {keyword: [{project, title, catalog_id, md_path, dependent_files, description}]}
109
+ {'index': {...}, 'stats': {...}} {'index': {}, 'stats': {...}}
110
+ 始终返回含 'index' 和 'stats' 两个 key 的 dict (调用方可安全 .get)。
111
+ wiki 不存在/为空/损坏时 index 为 {}, stats 计数为 0。
77
112
  """
78
- wiki_dirs = find_repowiki_dirs(code_dir)
79
- if not wiki_dirs:
80
- return {}
81
-
82
113
  index = {} # keyword -> [entry, ...]
83
114
  stats = {'projects': 0, 'pages': 0, 'keywords': 0}
84
115
 
85
- for project, wiki_root in wiki_dirs:
86
- meta = load_wiki_metadata(wiki_root)
87
- if not meta:
88
- continue
89
- stats['projects'] += 1
90
-
91
- catalogs = meta.get('wiki_catalogs', [])
92
- items = meta.get('wiki_items', [])
93
- # catalog_id -> title 映射
94
- id_to_title = {it.get('catalog_id'): it.get('title', '') for it in items}
95
-
96
- for cat in catalogs:
97
- cat_id = cat.get('id', '')
98
- name = cat.get('name', '')
99
- desc = cat.get('description', '')
100
- dep_files = cat.get('dependent_files', '')
101
- title = id_to_title.get(cat_id, name)
102
-
103
- # 找对应的 markdown 文档
104
- md_path = _find_md_for_catalog(wiki_root, cat_id, name)
105
- if not md_path:
106
- continue
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}
107
121
 
108
- stats['pages'] += 1
109
-
110
- # 提取关键词 (标题 + 描述 + 依赖文件名)
111
- kw_text = ' '.join([title, name, desc])
112
- # 依赖文件也作为关键词 (异常.java 搜"异常"能命中)
113
- if dep_files:
114
- kw_text += ' ' + dep_files.replace(',', ' ').replace('/', ' ')
115
- keywords = _extract_keywords(kw_text)
116
-
117
- entry = {
118
- 'project': project,
119
- 'title': title or name,
120
- 'catalog_id': cat_id,
121
- 'md_path': md_path,
122
- 'dependent_files': [f.strip() for f in dep_files.split(',') if f.strip()] if dep_files else [],
123
- 'description': desc,
124
- }
125
-
126
- for kw in keywords:
127
- index.setdefault(kw, [])
128
- if entry not in index[kw]:
129
- index[kw].append(entry)
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 出错, 跳过
130
179
 
131
180
  stats['keywords'] = len(index)
181
+ _write_wiki_index(output_path, index, stats)
182
+ return {'index': index, 'stats': stats}
132
183
 
133
- # 写入
134
- os.makedirs(os.path.dirname(output_path), exist_ok=True)
135
- with open(output_path, 'w', encoding='utf-8') as f:
136
- json.dump({'index': index, 'stats': stats}, f, ensure_ascii=False, indent=2)
137
184
 
138
- return {'index': index, 'stats': stats}
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
139
193
 
140
194
 
141
195
  def _find_md_for_catalog(wiki_root: str, catalog_id: str, name: str) -> Optional[str]:
142
196
  """在 content/ 目录下找对应的 markdown 文件。
143
197
 
144
198
  文件名通常等于 catalog name (.md), 在某个子分类目录下。
199
+ 健壮性: name 为空/content 目录不存在时返回 None。
145
200
  """
201
+ if not name:
202
+ return None
146
203
  content_dir = os.path.join(wiki_root, 'content')
147
204
  if not os.path.isdir(content_dir):
148
205
  return None