@hupan56/wlkj 2.3.0 → 2.3.2
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
|
@@ -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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
51
|
-
|
|
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
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
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
|
|
@@ -80,6 +80,26 @@ python .qoder/scripts/team_sync.py push
|
|
|
80
80
|
```
|
|
81
81
|
让团队/上级看到。
|
|
82
82
|
|
|
83
|
+
## 推送通知(可选,二选一)
|
|
84
|
+
|
|
85
|
+
生成报告后,如果用户要求"发到飞书/钉钉/群里",按环境选:
|
|
86
|
+
|
|
87
|
+
### 方式 A: QoderWork 连接器(QoderWork 桌面端,交互式)
|
|
88
|
+
如果你在 **QoderWork** 里运行(有连接器):
|
|
89
|
+
1. 检查用户是否装了飞书/钉钉连接器(Settings → Connectors & MCP)
|
|
90
|
+
2. 用连接器把报告发到指定群/个人
|
|
91
|
+
3. 这是最自然的方式(富文本卡片、@提及、审批流转都支持)
|
|
92
|
+
> 提示用户: "要我发到飞书吗?先在 QoderWork Settings → Connectors 装飞书连接器"
|
|
93
|
+
|
|
94
|
+
### 方式 B: webhook(任何环境,脚本化)
|
|
95
|
+
如果没有连接器,用引擎自带的 webhook 推送:
|
|
96
|
+
```bash
|
|
97
|
+
python .qoder/scripts/report.py daily --push-feishu
|
|
98
|
+
python .qoder/scripts/report.py weekly --push-feishu
|
|
99
|
+
```
|
|
100
|
+
这会通过 config.yaml 里配的飞书 bot webhook 发卡片通知。
|
|
101
|
+
两种方式不冲突,连接器更富交互,webhook 更可靠无依赖。
|
|
102
|
+
|
|
83
103
|
## 输出规则
|
|
84
104
|
|
|
85
105
|
- 报告是中文,简洁(日报 ≤20 行,周报 ≤40 行)
|