weflow-cli 1.6.1 → 1.6.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/CHANGELOG.md CHANGED
@@ -4,6 +4,24 @@ The npm package is published separately from GitHub. It may lag behind the `mast
4
4
 
5
5
  All notable user-facing changes are recorded here. This project follows [Semantic Versioning](https://semver.org/).
6
6
 
7
+ ## 1.6.2
8
+
9
+ ### Fixed
10
+
11
+ - Read every message shard, not just the configured one. WeChat rolls a conversation into a new `message_N.db` over time and encrypts each shard with its own PBKDF2-derived key. `sessions`, `messages`, `contacts`, `export json/txt/excel`, `evidence` and the MCP server opened only `message_0.db` with the single configured key, so anything written after the last shard roll was **invisible** - and the commands still reported success. On the machine this was found on, one conversation showed 31 messages ending 2026-08-30 instead of 1145 ending 2026-09-17. Only `export html` merged shards (its Python exporter always had), which is why the two read paths disagreed.
12
+ - `export <talker> html --limit N` now honours the limit. It reached only the fallback renderer; the primary path passed the flag nowhere, so a capped export of a busy chat silently produced the entire history.
13
+ - `export <talker> excel` works at all. `exceljs` is CJS-only, so `await import('exceljs')` yields a namespace whose only member is `default`; `new ExcelJS.Workbook()` on the namespace is a `TypeError`, which a bare `catch {}` turned into the uninformative "Excel 导出失败". The reason is now reported too.
14
+ - Group messages no longer show the sender's own `wxid_...:` prefix in the text (`波: [强]`, not `波: wxid_ogfiei1l1ye722: [强]`). Only `parsedContent` is normalised; `content`/`rawContent` keep the stored value.
15
+ - `contacts` no longer emits a blank row for `Name2Id`'s placeholder entry.
16
+ - `fav list` no longer blames the data channel when the actual blocker is a missing favorites key. Added `check --json` → `favoritesReady`, which distinguishes "database found" from "usable".
17
+ - Subprocess failures now include the last stderr line, redacted of key-shaped strings. A missing config key used to surface as `统计失败 (exit 1)` with no cause; it now names the cause and the command that fixes it.
18
+ - `daily-stats` / `daily` no longer tell users to run `config set bizKey`, which the CLI rejects as an unwritable key.
19
+ - `init --refresh` no longer fails with "密钥提取失败" when a perfectly good key is already configured. The hook only fires at the login moment, so a refresh run against an already-logged-in WeChat always times out; the fallback then consulted **only** `favPassphrase`, which `init` itself never writes (it writes `decryptKey`, and `favPassphrase` is set only when a usable `favorite.db` exists). Reads use `favPassphrase || decryptKey`, so the fallback rejected keys the rest of the tool was happily using. Reported as "最新版本的微信密钥解不开了" (Issue #9) after a WeChat upgrade, where `init --refresh` is exactly what the CLI tells users to run. The fallback now matches the read path, and the summary distinguishes a fresh capture ("密钥获取成功") from reusing what was configured ("已沿用配置中的密钥") instead of claiming success either way. Reaching that point also lets `enableFavorites` run, so favorites start working for users who previously died here.
20
+
21
+ ### Added
22
+
23
+ - `scripts/health_check.py` and `docs/HEALTH-CHECK.md`: a periodic, zero-token health check whose exit code is the verdict, covering version drift, shard read consistency, session freshness, export formats and the watcher tasks.
24
+
7
25
  ## 1.6.1
8
26
 
9
27
  ### Fixed
package/README.md CHANGED
@@ -322,6 +322,7 @@ python scripts/fav_server.py --date YYYY-MM-DD
322
322
  - [技术决策记录](./docs/DECISIONS.md):长期维护中的关键设计取舍。
323
323
  - [分支与合并记录](./docs/BRANCHES.md):分支用途、合并时间线与并行开发的对齐约定。
324
324
  - [发布清单](./docs/RELEASING.md):版本号、打包敏感内容检查、npm 凭据、国内镜像同步。
325
+ - [定期体检](./docs/HEALTH-CHECK.md):一条命令查出版本漂移、分片漏读、会话不更新与导出失败。
325
326
  - [电子数据证据指南](./docs/EVIDENCE_GUIDE.md):证据保全、法律边界与使用注意事项。
326
327
  - [贡献指南](./CONTRIBUTING.md) 与 [安全策略](./SECURITY.md):开发、反馈和敏感问题处理。
327
328
  - [变更记录](./CHANGELOG.md):版本更新摘要。
package/bin/weflow-cli.ts CHANGED
@@ -685,6 +685,8 @@ program
685
685
  console.log(chalk.gray(' 尝试从 WeFlow 桌面版配置读取...'))
686
686
  let extractedKey = opts.testMissingKeys ? '' : tryReadWeFlowKey()
687
687
  let linuxNtKey = ''
688
+ /** 密钥是本次从 hook 捕获的, 还是沿用已有的 - 摘要要说准, 不能一律报"获取成功" */
689
+ let keyCaptured = false
688
690
 
689
691
  if (extractedKey) {
690
692
  console.log(chalk.green(' ✓ 从 WeFlow 配置读取到密钥'))
@@ -770,11 +772,17 @@ program
770
772
 
771
773
  if (keyResult.success && keyResult.key) {
772
774
  extractedKey = keyResult.key
775
+ keyCaptured = true
773
776
  } else {
774
- // hook 只在登录瞬间触发; 微信已登录时捕获不到 → 回退到已配置的 passphrase
775
- const fallback = configService.get('favPassphrase') || ''
777
+ // hook 只在登录瞬间触发; 微信已登录时捕获不到 → 回退到已有密钥。
778
+ // decryptKey 也必须算数: init 成功时写入的就是它, 而 favPassphrase
779
+ // 仅在收藏库存在且验证通过时才会被设置。只看 favPassphrase 会让
780
+ // `init --refresh` 在一把完全可用的密钥面前报「密钥提取失败」——
781
+ // 而那正是 CLI 自己在微信迁移后建议用户执行的命令。
782
+ const fallback = configService.get('favPassphrase') || configService.get('decryptKey') || ''
776
783
  if (fallback) {
777
- console.log(chalk.gray(' Hook 未捕获到密钥 (微信已登录时不再触发密钥函数), 使用已配置的 passphrase 继续'))
784
+ console.log(chalk.gray(' Hook 未捕获到密钥 (微信已登录时不再触发密钥函数)'))
785
+ console.log(chalk.gray(' 检测到已配置的密钥, 沿用它并重新验证各库...'))
778
786
  extractedKey = fallback
779
787
  } else {
780
788
  console.log(chalk.red(`\n✗ 密钥提取失败: ${keyResult.error}`))
@@ -789,7 +797,8 @@ program
789
797
 
790
798
  if (extractedKey) {
791
799
  configService.set('decryptKey', extractedKey)
792
- console.log(chalk.green('\n✓ 密钥获取成功!'))
800
+ if (keyCaptured) console.log(chalk.green('\n✓ 密钥获取成功!'))
801
+ else console.log(chalk.green('\n✓ 已沿用配置中的密钥 (本次未重新捕获)'))
793
802
  } else {
794
803
  console.log(chalk.green('\n✓ NT 密钥配置完成!'))
795
804
  }
@@ -2519,14 +2528,26 @@ favCmd
2519
2528
  else console.log(chalk.red(`\n❌ 数据库连接失败: ${conn.error}\n`))
2520
2529
  process.exit(1)
2521
2530
  }
2522
- if (!chatService.isFavSupported()) {
2531
+ const favReason = chatService.favUnavailableReason()
2532
+ if (favReason) {
2533
+ const detail = {
2534
+ channel: '当前数据通道不支持收藏查询 (需 4.x NT 连接)',
2535
+ path: '未找到收藏数据库 (favorite.db),请确认微信已登录并产生过收藏',
2536
+ key: '收藏数据库已找到,但缺少密钥',
2537
+ }[favReason]
2523
2538
  if (opts.json) {
2524
- console.log(JSON.stringify({ success: false, code: 'FAVORITES_UNAVAILABLE', error: '当前数据通道不支持收藏查询' }))
2539
+ console.log(JSON.stringify({ success: false, code: 'FAVORITES_UNAVAILABLE', reason: favReason, error: detail }))
2525
2540
  process.exit(1)
2526
2541
  }
2527
- console.log(chalk.red('\n❌ 当前数据通道不支持收藏查询 (需 4.x NT 连接)'))
2528
- console.log(chalk.gray(' 请先配置收藏密钥: weflow-cli fav set-key <64位hex密钥>'))
2529
- console.log(chalk.gray(' 或设置全库 passphrase: weflow-cli fav set-key --passphrase <64位hex>\n'))
2542
+ console.log(chalk.red(`\n❌ ${detail}`))
2543
+ if (favReason === 'key') {
2544
+ console.log(chalk.gray(' 请先配置收藏密钥: weflow-cli fav set-key <64位hex密钥>'))
2545
+ console.log(chalk.gray(' 或设置全库 passphrase: weflow-cli fav set-key --passphrase <64位hex>'))
2546
+ }
2547
+ if (favReason === 'path') {
2548
+ console.log(chalk.gray(' 可手动指定: weflow-cli config set favDbPath <favorite.db 路径>'))
2549
+ }
2550
+ console.log('')
2530
2551
  process.exit(1)
2531
2552
  }
2532
2553
 
@@ -5362,6 +5383,12 @@ program
5362
5383
  messageDatabase: !!configService.get('ntDbPath') && existsSync(configService.get('ntDbPath')),
5363
5384
  momentsDatabase: !!configService.get('snsDbPath') && existsSync(configService.get('snsDbPath')),
5364
5385
  favoritesDatabase: !!configService.get('favDbPath') && existsSync(configService.get('favDbPath')),
5386
+ // `favoritesDatabase` only says the file is there. Reading it also
5387
+ // needs a key, and a caller that took the former for "usable" would
5388
+ // attempt favorites and always fail.
5389
+ favoritesReady: !!configService.get('favDbPath')
5390
+ && existsSync(configService.get('favDbPath'))
5391
+ && (!!configService.get('favKey') || !!configService.get('favPassphrase')),
5365
5392
  },
5366
5393
  assistant: {
5367
5394
  engine,
@@ -658,6 +658,8 @@ program
658
658
  console.log(chalk.gray(' 尝试从 WeFlow 桌面版配置读取...'));
659
659
  let extractedKey = opts.testMissingKeys ? '' : tryReadWeFlowKey();
660
660
  let linuxNtKey = '';
661
+ /** 密钥是本次从 hook 捕获的, 还是沿用已有的 - 摘要要说准, 不能一律报"获取成功" */
662
+ let keyCaptured = false;
661
663
  if (extractedKey) {
662
664
  console.log(chalk.green(' ✓ 从 WeFlow 配置读取到密钥'));
663
665
  }
@@ -744,12 +746,18 @@ program
744
746
  });
745
747
  if (keyResult.success && keyResult.key) {
746
748
  extractedKey = keyResult.key;
749
+ keyCaptured = true;
747
750
  }
748
751
  else {
749
- // hook 只在登录瞬间触发; 微信已登录时捕获不到 → 回退到已配置的 passphrase
750
- const fallback = configService.get('favPassphrase') || '';
752
+ // hook 只在登录瞬间触发; 微信已登录时捕获不到 → 回退到已有密钥。
753
+ // decryptKey 也必须算数: init 成功时写入的就是它, 而 favPassphrase
754
+ // 仅在收藏库存在且验证通过时才会被设置。只看 favPassphrase 会让
755
+ // `init --refresh` 在一把完全可用的密钥面前报「密钥提取失败」——
756
+ // 而那正是 CLI 自己在微信迁移后建议用户执行的命令。
757
+ const fallback = configService.get('favPassphrase') || configService.get('decryptKey') || '';
751
758
  if (fallback) {
752
- console.log(chalk.gray(' Hook 未捕获到密钥 (微信已登录时不再触发密钥函数), 使用已配置的 passphrase 继续'));
759
+ console.log(chalk.gray(' Hook 未捕获到密钥 (微信已登录时不再触发密钥函数)'));
760
+ console.log(chalk.gray(' 检测到已配置的密钥, 沿用它并重新验证各库...'));
753
761
  extractedKey = fallback;
754
762
  }
755
763
  else {
@@ -764,7 +772,10 @@ program
764
772
  }
765
773
  if (extractedKey) {
766
774
  configService.set('decryptKey', extractedKey);
767
- console.log(chalk.green('\n✓ 密钥获取成功!'));
775
+ if (keyCaptured)
776
+ console.log(chalk.green('\n✓ 密钥获取成功!'));
777
+ else
778
+ console.log(chalk.green('\n✓ 已沿用配置中的密钥 (本次未重新捕获)'));
768
779
  }
769
780
  else {
770
781
  console.log(chalk.green('\n✓ NT 密钥配置完成!'));
@@ -2588,14 +2599,26 @@ favCmd
2588
2599
  console.log(chalk.red(`\n❌ 数据库连接失败: ${conn.error}\n`));
2589
2600
  process.exit(1);
2590
2601
  }
2591
- if (!chatService.isFavSupported()) {
2602
+ const favReason = chatService.favUnavailableReason();
2603
+ if (favReason) {
2604
+ const detail = {
2605
+ channel: '当前数据通道不支持收藏查询 (需 4.x NT 连接)',
2606
+ path: '未找到收藏数据库 (favorite.db),请确认微信已登录并产生过收藏',
2607
+ key: '收藏数据库已找到,但缺少密钥',
2608
+ }[favReason];
2592
2609
  if (opts.json) {
2593
- console.log(JSON.stringify({ success: false, code: 'FAVORITES_UNAVAILABLE', error: '当前数据通道不支持收藏查询' }));
2610
+ console.log(JSON.stringify({ success: false, code: 'FAVORITES_UNAVAILABLE', reason: favReason, error: detail }));
2594
2611
  process.exit(1);
2595
2612
  }
2596
- console.log(chalk.red('\n❌ 当前数据通道不支持收藏查询 (需 4.x NT 连接)'));
2597
- console.log(chalk.gray(' 请先配置收藏密钥: weflow-cli fav set-key <64位hex密钥>'));
2598
- console.log(chalk.gray(' 或设置全库 passphrase: weflow-cli fav set-key --passphrase <64位hex>\n'));
2613
+ console.log(chalk.red(`\n❌ ${detail}`));
2614
+ if (favReason === 'key') {
2615
+ console.log(chalk.gray(' 请先配置收藏密钥: weflow-cli fav set-key <64位hex密钥>'));
2616
+ console.log(chalk.gray(' 或设置全库 passphrase: weflow-cli fav set-key --passphrase <64位hex>'));
2617
+ }
2618
+ if (favReason === 'path') {
2619
+ console.log(chalk.gray(' 可手动指定: weflow-cli config set favDbPath <favorite.db 路径>'));
2620
+ }
2621
+ console.log('');
2599
2622
  process.exit(1);
2600
2623
  }
2601
2624
  let favType;
@@ -5563,6 +5586,12 @@ program
5563
5586
  messageDatabase: !!configService.get('ntDbPath') && existsSync(configService.get('ntDbPath')),
5564
5587
  momentsDatabase: !!configService.get('snsDbPath') && existsSync(configService.get('snsDbPath')),
5565
5588
  favoritesDatabase: !!configService.get('favDbPath') && existsSync(configService.get('favDbPath')),
5589
+ // `favoritesDatabase` only says the file is there. Reading it also
5590
+ // needs a key, and a caller that took the former for "usable" would
5591
+ // attempt favorites and always fail.
5592
+ favoritesReady: !!configService.get('favDbPath')
5593
+ && existsSync(configService.get('favDbPath'))
5594
+ && (!!configService.get('favKey') || !!configService.get('favPassphrase')),
5566
5595
  },
5567
5596
  assistant: {
5568
5597
  engine,