weflow-cli 1.9.0 → 1.11.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.
- package/ARCHITECTURE.md +62 -26
- package/CHANGELOG.md +1051 -0
- package/OPERATIONS.md +99 -13
- package/README.en.md +14 -5
- package/README.md +23 -8
- package/bin/weflow-cli.ts +364 -40
- package/dist/bin/weflow-cli.js +381 -69
- package/dist/bin/weflow-cli.js.map +1 -1
- package/dist/mcp-server/index.d.ts.map +1 -1
- package/dist/mcp-server/index.js +19 -1
- package/dist/mcp-server/index.js.map +1 -1
- package/dist/src/core/dbPathService.d.ts +12 -0
- package/dist/src/core/dbPathService.d.ts.map +1 -1
- package/dist/src/core/dbPathService.js +20 -3
- package/dist/src/core/dbPathService.js.map +1 -1
- package/dist/src/panel/server.d.ts.map +1 -1
- package/dist/src/panel/server.js +35 -0
- package/dist/src/panel/server.js.map +1 -1
- package/dist/src/services/assistantService.d.ts +19 -0
- package/dist/src/services/assistantService.d.ts.map +1 -1
- package/dist/src/services/assistantService.js +20 -0
- package/dist/src/services/assistantService.js.map +1 -1
- package/dist/src/services/assistantTools.d.ts +27 -1
- package/dist/src/services/assistantTools.d.ts.map +1 -1
- package/dist/src/services/assistantTools.js +169 -35
- package/dist/src/services/assistantTools.js.map +1 -1
- package/dist/src/services/configService.d.ts +2 -0
- package/dist/src/services/configService.d.ts.map +1 -1
- package/dist/src/services/configService.js +3 -2
- package/dist/src/services/configService.js.map +1 -1
- package/dist/src/services/exportService.d.ts +43 -0
- package/dist/src/services/exportService.d.ts.map +1 -1
- package/dist/src/services/exportService.js +116 -11
- package/dist/src/services/exportService.js.map +1 -1
- package/dist/src/services/messageQuery.d.ts +18 -0
- package/dist/src/services/messageQuery.d.ts.map +1 -1
- package/dist/src/services/messageQuery.js +29 -0
- package/dist/src/services/messageQuery.js.map +1 -1
- package/dist/src/services/wechatClient.d.ts +125 -0
- package/dist/src/services/wechatClient.d.ts.map +1 -0
- package/dist/src/services/wechatClient.js +344 -0
- package/dist/src/services/wechatClient.js.map +1 -0
- package/dist/src/services/wechatMedia.d.ts +44 -0
- package/dist/src/services/wechatMedia.d.ts.map +1 -0
- package/dist/src/services/wechatMedia.js +155 -0
- package/dist/src/services/wechatMedia.js.map +1 -0
- package/dist/src/services/wechatMessageService.d.ts +11 -0
- package/dist/src/services/wechatMessageService.d.ts.map +1 -1
- package/dist/src/services/wechatMessageService.js +55 -11
- package/dist/src/services/wechatMessageService.js.map +1 -1
- package/dist/src/types.d.ts +4 -0
- package/dist/src/types.d.ts.map +1 -1
- package/docs/CONTACT_DB_SCHEMA.md +1085 -0
- package/docs/DECISIONS.md +1274 -0
- package/docs/EXTENDING.md +69 -7
- package/docs/FUNCTIONAL-IMPROVEMENT-SPEC.md +27 -0
- package/docs/HEALTH-CHECK.md +12 -7
- package/docs/JEV/345/272/224/347/224/250/346/226/275/345/267/245/346/226/207/346/241/243.md +2 -2
- package/docs/MCP.md +2 -2
- package/docs/PROJECT_STATE.md +64 -5
- package/docs/RELEASING.md +128 -1
- package/docs/images/weflow-architecture-gpt-image-2.png +0 -0
- package/logo.png +0 -0
- package/mcp-server/index.ts +18 -3
- package/package.json +5 -3
- package/resources/js/graph3d/NOTICE.txt +35 -0
- package/resources/js/graph3d/d3-binarytree.min.js +2 -0
- package/resources/js/graph3d/d3-dispatch.min.js +2 -0
- package/resources/js/graph3d/d3-force-3d.min.js +2 -0
- package/resources/js/graph3d/d3-octree.min.js +2 -0
- package/resources/js/graph3d/d3-quadtree.min.js +2 -0
- package/resources/js/graph3d/d3-timer.min.js +2 -0
- package/resources/js/graph3d/layout.mjs +63 -0
- package/resources/js/graph3d/three.min.js +6 -0
- package/resources/js/graph3d/viewer2d.js +448 -0
- package/resources/panel/index.html +12 -0
- package/resources/panel/mascot-blink-1.png +0 -0
- package/resources/panel/mascot-blink-2.png +0 -0
- package/resources/panel/mascot-lift-down-1.png +0 -0
- package/resources/panel/mascot-lift-down-2.png +0 -0
- package/resources/panel/mascot-lift-down-3.png +0 -0
- package/resources/panel/mascot-lift-down-4.png +0 -0
- package/resources/panel/mascot-lift-down-5.png +0 -0
- package/resources/panel/mascot-lift-down-6.png +0 -0
- package/resources/panel/mascot-lift-up-1.png +0 -0
- package/resources/panel/mascot-lift-up-2.png +0 -0
- package/resources/panel/mascot-lift-up-3.png +0 -0
- package/resources/panel/mascot-lift-up-4.png +0 -0
- package/resources/panel/mascot-lift-up-5.png +0 -0
- package/resources/panel/mascot-lift-up-6.png +0 -0
- package/resources/panel/mascot-tickle-1.png +0 -0
- package/resources/panel/mascot-tickle-2.png +0 -0
- package/resources/panel/mascot-tickle-3.png +0 -0
- package/resources/panel/mascot-tickle-4.png +0 -0
- package/resources/panel/panel.css +111 -0
- package/resources/panel/renderer.js +480 -8
- package/scripts/_utils.py +1203 -977
- package/scripts/biz_daily.py +25 -10
- package/scripts/chat_notes.py +15 -5
- package/scripts/check_readme_commands.py +426 -0
- package/scripts/compile_wiki.py +163 -57
- package/scripts/contact_schema.py +339 -0
- package/scripts/data/wechat_emoji.json +225 -0
- package/scripts/export_chat_html.py +346 -6
- package/scripts/fav_notes.py +4 -1
- package/scripts/graph_2d.py +151 -0
- package/scripts/graph_3d.py +646 -0
- package/scripts/health_check.py +163 -26
- package/scripts/nt_decrypt.py +3 -3
- package/scripts/panel_frames.py +683 -0
- package/scripts/pipeline.py +21 -10
- package/scripts/quality_eval.py +5 -0
- package/scripts/rag_chat.py +33 -16
- package/scripts/reply_debt.py +26 -6
- package/scripts/semantic_search.py +212 -58
- package/scripts/vault_rag.py +42 -16
- package/scripts/vault_search.py +44 -8
- package/scripts/wechat_emoticon.py +6 -0
- package/scripts/wiki_lint.py +94 -17
- package/src/core/dbPathService.ts +22 -4
- package/src/panel/server.ts +33 -0
- package/src/services/assistantService.ts +26 -0
- package/src/services/assistantTools.ts +2452 -2319
- package/src/services/configService.ts +5 -2
- package/src/services/exportService.ts +129 -12
- package/src/services/messageQuery.ts +29 -0
- package/src/services/wechatClient.ts +380 -0
- package/src/services/wechatMedia.ts +166 -0
- package/src/services/wechatMessageService.ts +62 -11
- package/src/types.ts +4 -0
- package/dist/src/utils/wechatEmoji.d.ts +0 -5
- package/dist/src/utils/wechatEmoji.d.ts.map +0 -1
- package/dist/src/utils/wechatEmoji.js +0 -35
- package/dist/src/utils/wechatEmoji.js.map +0 -1
package/scripts/health_check.py
CHANGED
|
@@ -10,6 +10,8 @@ thing has silently broken at least once, and the failure was invisible:
|
|
|
10
10
|
导出格式 excel 全程从未成功过, 报错只有"导出失败"四个字
|
|
11
11
|
Python 依赖 缺 sqlcipher3 时命令报的是"数据库连接失败"
|
|
12
12
|
计划任务 提醒脚本静默停跑, 没人会发现
|
|
13
|
+
微信客户端 从没报过客户端版本; 而"读 exe 里的版本字面量"是错的判据(实测那只是兜底),
|
|
14
|
+
磁盘上还可能留着一个不能用的版本目录(指向它的加载会失败)
|
|
13
15
|
|
|
14
16
|
Run it ad hoc, or schedule it and read the summary line:
|
|
15
17
|
|
|
@@ -136,14 +138,41 @@ def check_environment(report):
|
|
|
136
138
|
|
|
137
139
|
|
|
138
140
|
def pick_talkers(report):
|
|
139
|
-
"""The most recently active sessions - the likeliest to span shards.
|
|
141
|
+
"""The most recently active sessions - the likeliest to span shards.
|
|
142
|
+
|
|
143
|
+
**挑不出来时要说出来**:没有会话是"没登录/没 init"(会报红),而**有会话、却一条都拿不到
|
|
144
|
+
`username`** —— 那是"字段长变了"或"列表是半截的"。这一支原来直接返回空,`main()` 里
|
|
145
|
+
`if talkers:` 于是**静默跳过**后两项检查,报告照样写"全部通过"(只是项数悄悄少了两个)。
|
|
146
|
+
同一个假绿在"计划任务"那一项也出现过(见那一段的注释)。
|
|
147
|
+
"""
|
|
140
148
|
_, data = run_json(['sessions', '--limit', str(SAMPLE_LIMIT)])
|
|
141
149
|
sessions = ((data or {}).get('sessions') or [])
|
|
142
150
|
if not sessions:
|
|
143
151
|
report.add('选择样本会话', False, '会话列表为空', '先确认微信已登录且 init 成功')
|
|
144
152
|
return []
|
|
145
153
|
ordered = sorted(sessions, key=lambda s: s.get('sortTimestamp') or 0, reverse=True)
|
|
146
|
-
|
|
154
|
+
picked = [s['username'] for s in ordered[:2] if s.get('username')]
|
|
155
|
+
if not picked:
|
|
156
|
+
# 有会话却挑不出样本 ⇒ 后面两项(读取路径一致 / 导出格式)会整段不跑,
|
|
157
|
+
# 不说的话这一份报告就是"少了两个检查的全绿"
|
|
158
|
+
report.add('选择样本会话', False, f'会话有 {len(sessions)} 条,但都没有 username 字段',
|
|
159
|
+
'sessions 的输出字段可能变了 —— 这两项检查因此没跑,不代表通过')
|
|
160
|
+
return picked
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def read_newest_json(out_dir):
|
|
164
|
+
"""那个目录里 json 导出的最新消息时间(秒);读不出来给 None。"""
|
|
165
|
+
newest = None
|
|
166
|
+
for path in Path(out_dir).glob('*.json'):
|
|
167
|
+
try:
|
|
168
|
+
rows = json.loads(path.read_text(encoding='utf-8'))
|
|
169
|
+
except (OSError, ValueError):
|
|
170
|
+
continue
|
|
171
|
+
times = [int(r.get('createTime') or 0) for r in rows]
|
|
172
|
+
if times:
|
|
173
|
+
newest = max(times)
|
|
174
|
+
break
|
|
175
|
+
return newest
|
|
147
176
|
|
|
148
177
|
|
|
149
178
|
def check_read_paths(report, talker):
|
|
@@ -152,21 +181,24 @@ def check_read_paths(report, talker):
|
|
|
152
181
|
They disagreed by 1114 messages when the JSON path read only the first
|
|
153
182
|
shard, so comparing their newest message is the cheapest way to notice the
|
|
154
183
|
read paths drifting apart again.
|
|
184
|
+
|
|
185
|
+
**但"最新消息"是会动的**:json 先导、html 后导,中间隔着一两分钟 —— 这期间聊天里来了
|
|
186
|
+
一条新消息,两条路径就会"不一致",而那是**聊天活着**,不是读取断了。原来判据是精确到分钟
|
|
187
|
+
的相等(2026-10-11 实测撞到过:`json 13:38 / html 13:40:14` 判红,紧接着同一条聊天
|
|
188
|
+
`13:40 vs 13:40:14` 判绿)。放宽容差解决不了:繁忙的群里分片轮转本来就在分钟级,
|
|
189
|
+
放宽就会把这个检查唯一要抓的东西放过去。
|
|
190
|
+
|
|
191
|
+
所以不一致时**再读一次 json 做仲裁**:那一读能看到 html 那条 ⇒ 差别来自"消息是新的";
|
|
192
|
+
看不到 ⇒ 两条路径真的不一致(这才是要报的)。仲裁只在**本来就不一致**时才花这一次导出。
|
|
155
193
|
"""
|
|
156
194
|
with tempfile.TemporaryDirectory(prefix='wf-health-') as tmp:
|
|
157
195
|
out_json = Path(tmp) / 'json'
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
except (OSError, ValueError):
|
|
165
|
-
continue
|
|
166
|
-
times = [int(r.get('createTime') or 0) for r in rows]
|
|
167
|
-
if times:
|
|
168
|
-
newest_json = max(times)
|
|
169
|
-
break
|
|
196
|
+
# 这次导出的退出码与 stderr **不看**:这一项判的是"两条路径读到的内容一致不一致",
|
|
197
|
+
# 而 json 读不出来时 `read_newest_json` 会给 None、下面照样报红。留两个不用的变量
|
|
198
|
+
# 只会让"检查了什么"看起来比实际多。
|
|
199
|
+
run(['export', talker, 'json', '--limit', '5',
|
|
200
|
+
'--output', str(out_json), '--non-interactive'])
|
|
201
|
+
newest_json = read_newest_json(out_json)
|
|
170
202
|
|
|
171
203
|
out_html = Path(tmp) / 'html'
|
|
172
204
|
code_h, out_h, _ = run(['export', talker, 'html', '--limit', '5',
|
|
@@ -193,14 +225,37 @@ def check_read_paths(report, talker):
|
|
|
193
225
|
return
|
|
194
226
|
|
|
195
227
|
newest_txt = datetime.datetime.fromtimestamp(newest_json).strftime('%Y-%m-%d %H:%M')
|
|
196
|
-
if newest_html:
|
|
197
|
-
match = newest_html[:16] == newest_txt
|
|
198
|
-
report.add(label, match,
|
|
199
|
-
f'json 最新 {newest_txt} / html 最新 {newest_html}',
|
|
200
|
-
'两条路径读到不同的最新消息,通常又只剩一个分片被读了')
|
|
201
|
-
else:
|
|
228
|
+
if not newest_html:
|
|
202
229
|
# No timestamp in the page (empty chat); the count comparison still holds.
|
|
203
230
|
report.add(label, total_html > 0, f'json 最新 {newest_txt} / html 共 {total_html} 条')
|
|
231
|
+
return
|
|
232
|
+
|
|
233
|
+
if newest_html[:16] == newest_txt:
|
|
234
|
+
report.add(label, True, f'json 最新 {newest_txt} / html 最新 {newest_html}')
|
|
235
|
+
return
|
|
236
|
+
|
|
237
|
+
# 不一致 —— **仲裁**:再读一次 json。它能看到 html 那条,说明差别只是"消息是新的"。
|
|
238
|
+
with tempfile.TemporaryDirectory(prefix='wf-health-arbiter-') as tmp:
|
|
239
|
+
again = Path(tmp) / 'json'
|
|
240
|
+
run(['export', talker, 'json', '--limit', '5',
|
|
241
|
+
'--output', str(again), '--non-interactive'])
|
|
242
|
+
arbiter = read_newest_json(again)
|
|
243
|
+
if arbiter is not None and arbiter >= newest_json:
|
|
244
|
+
arbiter_txt = datetime.datetime.fromtimestamp(arbiter).strftime('%Y-%m-%d %H:%M')
|
|
245
|
+
if arbiter_txt == newest_html[:16]:
|
|
246
|
+
report.add(label, True,
|
|
247
|
+
f'json 最新 {newest_txt} → 复查 {arbiter_txt} / html 最新 {newest_html}',
|
|
248
|
+
'差别来自检查期间来了新消息(这一项防的是"只剩一个分片被读了")')
|
|
249
|
+
return
|
|
250
|
+
|
|
251
|
+
# 仲裁没说通(复查也没读到 html 那条,或者根本没读出来)⇒ 这是真的不一致
|
|
252
|
+
if arbiter is None:
|
|
253
|
+
reason = '复查 json 也没读出来'
|
|
254
|
+
else:
|
|
255
|
+
reason = '复查 json 仍是 %s,没有更近的消息' % \
|
|
256
|
+
datetime.datetime.fromtimestamp(arbiter).strftime('%Y-%m-%d %H:%M')
|
|
257
|
+
report.add(label, False, f'json 最新 {newest_txt} / html 最新 {newest_html}',
|
|
258
|
+
'两条路径读到不同的最新消息,通常又只剩一个分片被读了(%s)' % reason)
|
|
204
259
|
|
|
205
260
|
|
|
206
261
|
def html_newest_stamp(out_dir):
|
|
@@ -219,7 +274,13 @@ def html_newest_stamp(out_dir):
|
|
|
219
274
|
|
|
220
275
|
|
|
221
276
|
def check_session_freshness(report):
|
|
222
|
-
"""A read path that silently stops at an old shard shows up here first.
|
|
277
|
+
"""A read path that silently stops at an old shard shows up here first.
|
|
278
|
+
|
|
279
|
+
⚠️ **但"最旧"与"只是没聊天"是同一个观测**:这一项看的是会话列表里最新那条消息有多旧,
|
|
280
|
+
而一个人一周没发消息,读数与"读取停在某个旧分片"**完全一样**。所以提示里必须把两种解释
|
|
281
|
+
都说出来,并且点出判别器在隔壁:`读取路径一致` 绿着 ⇒ 多半只是这段时间没聊天;
|
|
282
|
+
两项一起红才更像读取断了。**别在这里替人下结论。**
|
|
283
|
+
"""
|
|
223
284
|
_, data = run_json(['sessions', '--limit', '5'])
|
|
224
285
|
sessions = ((data or {}).get('sessions') or [])
|
|
225
286
|
stamps = [s.get('sortTimestamp') or 0 for s in sessions]
|
|
@@ -231,7 +292,9 @@ def check_session_freshness(report):
|
|
|
231
292
|
when = datetime.datetime.fromtimestamp(newest).strftime('%Y-%m-%d %H:%M')
|
|
232
293
|
report.add('会话新鲜度', age <= STALE_SESSION_DAYS,
|
|
233
294
|
f'最新会话 {when}({age:.1f} 天前)',
|
|
234
|
-
f'超过 {STALE_SESSION_DAYS}
|
|
295
|
+
f'超过 {STALE_SESSION_DAYS} 天没有新消息。两种解释:**这段时间没聊天**(最可能)'
|
|
296
|
+
f'或**读取只覆盖了部分分片** —— 看「读取路径一致」那一项:它绿着就多半是前者,'
|
|
297
|
+
f'两项一起红才更像读取断了')
|
|
235
298
|
|
|
236
299
|
|
|
237
300
|
def check_export_formats(report, talker):
|
|
@@ -267,7 +330,13 @@ def check_python_deps(report):
|
|
|
267
330
|
|
|
268
331
|
|
|
269
332
|
def check_scheduled_tasks(report):
|
|
270
|
-
"""The watcher tasks must exist and their last run must have succeeded.
|
|
333
|
+
"""The watcher tasks must exist and their last run must have succeeded.
|
|
334
|
+
|
|
335
|
+
**"读不出来"必须与"上次成功"分开**:这一项的判据是从 `schtasks` 的文本输出里找
|
|
336
|
+
`Last Result` / `上次运行结果` 那一行 —— 系统语言或输出格式一变,那一行就找不到了,
|
|
337
|
+
而**两个清单都会是空的**,于是它落到 `else`、报"均在位且上次成功"。
|
|
338
|
+
那正是把"没验证"说成"通过"(同一类:`docs/DECISIONS.md` D-107 里那条"取不到就说未知")。
|
|
339
|
+
"""
|
|
271
340
|
if os.name != 'nt':
|
|
272
341
|
report.add('计划任务', True, '非 Windows,跳过')
|
|
273
342
|
return
|
|
@@ -275,7 +344,7 @@ def check_scheduled_tasks(report):
|
|
|
275
344
|
report.add('计划任务', True, '找不到 schtasks,跳过')
|
|
276
345
|
return
|
|
277
346
|
names = ['WeFlow Issue Watch', 'WeFlow Mail Watch']
|
|
278
|
-
broken, absent = [], []
|
|
347
|
+
broken, absent, unreadable, not_yet_run = [], [], [], []
|
|
279
348
|
for name in names:
|
|
280
349
|
try:
|
|
281
350
|
proc = subprocess.run(['schtasks', '/query', '/tn', name, '/fo', 'LIST', '/v'],
|
|
@@ -287,21 +356,88 @@ def check_scheduled_tasks(report):
|
|
|
287
356
|
absent.append(name)
|
|
288
357
|
continue
|
|
289
358
|
text = proc.stdout or ''
|
|
359
|
+
result = None
|
|
290
360
|
for line in text.split('\n'):
|
|
291
361
|
if 'Last Result' in line or '上次运行结果' in line:
|
|
292
362
|
result = line.split(':', 1)[-1].strip()
|
|
293
|
-
|
|
294
|
-
|
|
363
|
+
break
|
|
364
|
+
if result is None:
|
|
365
|
+
# 输出里**没有那个字段**:要么语言/格式变了,要么它没在汇报这一项。
|
|
366
|
+
# 这时该说的是"读不出来",不是"上次成功"。
|
|
367
|
+
unreadable.append(name)
|
|
368
|
+
elif result in ('267011', '0x41303'):
|
|
369
|
+
# `SCHED_S_TASK_HAS_NOT_RUN`(267011 / 0x41303)—— **不是失败**:注册之后还没到
|
|
370
|
+
# 第一次触发。原来它落进下面那支"上次运行失败",提示还写着"脚本可能抛异常了",把刚
|
|
371
|
+
# 注册的人引去查一个不存在的异常(2026-10-11 本机注册这两个任务时实测到)。
|
|
372
|
+
# 但也**不算通过**:这一项要的是"跑过一次且成功",而任务一直"没跑过"正是它要防的事
|
|
373
|
+
# (触发条件写坏,或设成了"仅在用户登录时运行"而没人登录)。
|
|
374
|
+
not_yet_run.append(name)
|
|
375
|
+
elif result not in ('0', '0x0'):
|
|
376
|
+
broken.append(f'{name}={result}')
|
|
295
377
|
if broken:
|
|
296
378
|
report.add('计划任务', False, f'上次运行失败: {", ".join(broken)}',
|
|
297
379
|
'脚本可能抛异常了,手动跑一次看输出')
|
|
298
380
|
elif absent:
|
|
299
381
|
report.add('计划任务', False, f'不存在: {", ".join(absent)}',
|
|
300
382
|
'提醒不会触发;见 docs/HEALTH-CHECK.md 的注册命令')
|
|
383
|
+
elif unreadable:
|
|
384
|
+
# **不算通过**:这一项没被验证过(不是"上次成功")
|
|
385
|
+
report.add('计划任务', False, f'读不出上次运行结果: {", ".join(unreadable)}',
|
|
386
|
+
'schtasks 的输出格式或系统语言变了 —— 这一项**没有验证**,不代表成功;'
|
|
387
|
+
'手动跑 `schtasks /query /tn "WeFlow Issue Watch" /fo LIST /v` 看一眼')
|
|
388
|
+
elif not_yet_run:
|
|
389
|
+
report.add('计划任务', False, f'还没跑过: {", ".join(not_yet_run)}',
|
|
390
|
+
'不算通过 —— 这一项要的是"跑过一次且成功"。刚注册的话等一次触发(30 分钟);'
|
|
391
|
+
'一直如此就查触发条件,以及任务是不是设成了"仅在用户登录时运行"')
|
|
301
392
|
else:
|
|
302
393
|
report.add('计划任务', True, '均在位且上次成功')
|
|
303
394
|
|
|
304
395
|
|
|
396
|
+
def check_wechat_client(report):
|
|
397
|
+
"""**微信客户端版本**,以及磁盘上有没有"不能用的版本目录"。
|
|
398
|
+
|
|
399
|
+
判据在 CLI 那一侧(`src/services/wechatClient.ts`):版本来自**活进程命令行的
|
|
400
|
+
`--user-lib-dir`** —— 不是 exe 里的版本字面量(实测那只是兜底),也不是"磁盘上有没有
|
|
401
|
+
这个版本目录"(本机就有一个只剩 1 个文件的残留目录,指向它的加载会失败)。
|
|
402
|
+
|
|
403
|
+
取不到就写"未知",**不写"没问题"**:与仓库里那条"没找到必须报 UNKNOWN,别报 CLEAR"同一条纪律。
|
|
404
|
+
字段整个不在 = CLI 是该重建的旧版本 ⇒ **不算通过**(这正是"打开成功≠能用"那一类:
|
|
405
|
+
少了一项却一片绿)。
|
|
406
|
+
"""
|
|
407
|
+
_, data = run_json(['check'])
|
|
408
|
+
if not data:
|
|
409
|
+
report.add('微信客户端', False, 'check 未返回 JSON', '先跑 weflow-cli check 看原始输出')
|
|
410
|
+
return
|
|
411
|
+
client = data.get('wechatClient')
|
|
412
|
+
if not isinstance(client, dict):
|
|
413
|
+
report.add('微信客户端', False, 'check 没报这一项',
|
|
414
|
+
'CLI 可能没重新构建(npm run build),或 dist 是旧的')
|
|
415
|
+
return
|
|
416
|
+
version = client.get('version')
|
|
417
|
+
loaded = client.get('loadedDirs') or []
|
|
418
|
+
if version:
|
|
419
|
+
detail = str(version) + (f'({loaded[0]})' if loaded else '')
|
|
420
|
+
else:
|
|
421
|
+
detail = '未知 —— ' + str(client.get('note') or '')
|
|
422
|
+
dead = client.get('unusableVersionDirs') or []
|
|
423
|
+
if dead:
|
|
424
|
+
shown = '、'.join(f'{d.get("dir")}({d.get("files")} 个文件)' for d in dead)
|
|
425
|
+
detail += f' ⚠ 残留版本目录(没有 Weixin.dll,加载会失败): {shown}'
|
|
426
|
+
# 客户端自报的数据目录 vs 配置推出来的根:不一致 = 两边可能不是同一份库(CLI 那边已按真身比过,
|
|
427
|
+
# 本机数据目录有 D 盘软链,所以这里只报结论)
|
|
428
|
+
if client.get('dataDirMatchesConfig') is False:
|
|
429
|
+
detail += ' ⚠ 客户端自报的数据目录与配置里那个数据库推出来的根**不是同一份**'
|
|
430
|
+
crashes = client.get('crashRecords') or {}
|
|
431
|
+
count = int(crashes.get('reports') or 0) + int(crashes.get('records') or 0)
|
|
432
|
+
if count:
|
|
433
|
+
newest = str(crashes.get('newest') or '时间读不到')[:16]
|
|
434
|
+
detail += f' · 客户端崩溃记录 {count} 份(最近 {newest})—— 崩过时数据读不出来先看这条'
|
|
435
|
+
# `update.data` 在场 ⇒ 上面那个版本**未必**是加载的那个:这句必须跟着版本一起出现
|
|
436
|
+
for caveat in client.get('caveats') or []:
|
|
437
|
+
detail += f' ⚠ {caveat}'
|
|
438
|
+
report.add('微信客户端', True, detail)
|
|
439
|
+
|
|
440
|
+
|
|
305
441
|
def main():
|
|
306
442
|
parser = argparse.ArgumentParser(description='weflow-cli periodic health check')
|
|
307
443
|
parser.add_argument('--json', action='store_true', help='机器可读输出')
|
|
@@ -314,6 +450,7 @@ def main():
|
|
|
314
450
|
report = Report()
|
|
315
451
|
check_version(report)
|
|
316
452
|
check_environment(report)
|
|
453
|
+
check_wechat_client(report)
|
|
317
454
|
check_python_deps(report)
|
|
318
455
|
check_session_freshness(report)
|
|
319
456
|
|
package/scripts/nt_decrypt.py
CHANGED
|
@@ -91,7 +91,7 @@ def _is_linux_wechat_process(pid):
|
|
|
91
91
|
if pid == os.getpid():
|
|
92
92
|
return False
|
|
93
93
|
try:
|
|
94
|
-
with open(f'/proc/{pid}/comm') as f:
|
|
94
|
+
with open(f'/proc/{pid}/comm', encoding='utf-8') as f:
|
|
95
95
|
comm = f.read().strip().lower()
|
|
96
96
|
if comm in _LINUX_WECHAT_COMMS:
|
|
97
97
|
return True
|
|
@@ -122,7 +122,7 @@ def find_weixin_pid_linux():
|
|
|
122
122
|
if not _is_linux_wechat_process(pid):
|
|
123
123
|
continue
|
|
124
124
|
try:
|
|
125
|
-
with open(f'/proc/{pid}/statm') as f:
|
|
125
|
+
with open(f'/proc/{pid}/statm', encoding='utf-8') as f:
|
|
126
126
|
rss_kb = int(f.read().split()[1]) * 4
|
|
127
127
|
except (OSError, IndexError, ValueError):
|
|
128
128
|
rss_kb = 0
|
|
@@ -205,7 +205,7 @@ def scan_memory_keys_linux(pid):
|
|
|
205
205
|
"""
|
|
206
206
|
regions = []
|
|
207
207
|
try:
|
|
208
|
-
with open(f'/proc/{pid}/maps') as f:
|
|
208
|
+
with open(f'/proc/{pid}/maps', encoding='utf-8') as f:
|
|
209
209
|
for line in f:
|
|
210
210
|
parts = line.split()
|
|
211
211
|
if len(parts) < 2 or 'r' not in parts[1]:
|