weflow-cli 1.14.1 → 1.15.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/CHANGELOG.md CHANGED
@@ -17,6 +17,30 @@ purpose** - a hand-written "latest is X" is exactly the claim that goes stale he
17
17
 
18
18
  All notable user-facing changes are recorded here. This project follows [Semantic Versioning](https://semver.org/).
19
19
 
20
+ ## 1.15.0
21
+
22
+ - **Four read-only answers to questions WeChat cannot answer.** `weflow-cli today` shows what you were
23
+ talking about on this day in earlier years (it walks every shard and every conversation table, so
24
+ "five years ago today" is actually reachable). `network` gives shared groups and introduction paths
25
+ from `contact.db` alone - it never opens a message. `promises --days 180` finds what you said you
26
+ would do, then looks for the follow-up evidence in the same conversation: you mentioning it was sent,
27
+ the other side chasing it, or the other side confirming. `timeline --source <账号>` lays one account's
28
+ articles out in time so its positions can be compared. All four are `--json`-capable, read-only, and
29
+ speak Chinese like the rest of the CLI.
30
+ - **Voice messages are searchable.** `search --phrase <词> --line all` now matches what was said out
31
+ loud, a content class WeChat cannot search at all. The blocker was not dialect but Whisper's default
32
+ self-conditioning: with `condition_on_previous_text` off, VAD on, a compression-ratio fallback and a
33
+ repetition penalty, the same clips that produced `我做的事情都没有做过` three times instead read
34
+ `你填一下交通补贴20元 兄弟`. Measured 4 of 5 clips readable afterwards, 0 of 5 before. Transcripts are
35
+ merged into the **phrase** index as `type='voice'` and deliberately kept out of the semantic one -
36
+ paying a second embedding bill for a dialect-approximate transcript is a bad trade.
37
+ - **Two silent name-resolution bugs fixed along the way.** Contacts live in `contact.db`, never in the
38
+ message shards, so looking for a `contact` table there yields a blank name map and every group prints
39
+ as `47966561144@chatroom`; and the configured `wxid` may carry a suffix that `Name2Id` does not
40
+ (`wxid_账号A_后缀` vs `wxid_账号A`), which made "messages I sent" match nothing until the comparison
41
+ became prefix-tolerant. `voice_transcribe.py` decodes, caches (content-addressed, resumable) and
42
+ writes `output/.voice_index/voices.jsonl`.
43
+
20
44
  ## 1.14.1
21
45
 
22
46
  - **The daily run now keeps the retrieval indexes fresh.** On 2026-10-22 the semantic index was
package/README.md CHANGED
@@ -296,6 +296,10 @@ weflow-cli config set assistantGroupRequireMention true
296
296
  | 谁在等我回话 | `weflow-cli awaiting --dry-run` · `weflow-cli awaiting --yes` |
297
297
  | 帮我起草回复(默认只出文字) | `weflow-cli draft <联系人> --dry-run` · `--yes`(**只有** `--pick N --send --yes` 会真的替你发出去;两步缺一不可)|
298
298
  | 关系温度 | `weflow-cli bonds`(谁在冷却、谁在变热、谁只有微信这一条线;只读本地库,不联网、不调模型)|
299
+ | 我说过要给谁做什么 | `weflow-cli promises --days 180`(带回原话与后续证据,规则判定,不调模型)|
300
+ | 那年今日 | `weflow-cli today --years 1,3,5`(几年前的今天在聊什么)|
301
+ | 人脉图谱 | `weflow-cli network --groups` · `--with <名字>`(共同群)· `--intro <名字>`(谁能引荐;只读 contact.db)|
302
+ | 某号观点演变 | `weflow-cli timeline --list` · `--source <公众号名>`(按时间看它说法的变化)|
299
303
  | 本机判断层 | `weflow-cli decide --request <file> --dry-run` · `--yes` |
300
304
  | 导出聊天记录 | `weflow-cli export <联系人> <json\|txt\|html\|excel>` |
301
305
  | 同步检查点 | `weflow-cli sync run <联系人> --since <日期>` · `sync status` · `sync verify` |
package/bin/weflow-cli.ts CHANGED
@@ -7438,6 +7438,175 @@ program
7438
7438
  }
7439
7439
  })
7440
7440
 
7441
+ // ==================== today(那年今日)====================
7442
+ program
7443
+ .command('today')
7444
+ .description('那年今日:几年前的今天你在聊什么(只读本地库,不联网)')
7445
+ .option('--years <list>', '往前看几年,逗号分隔', '1,3,5')
7446
+ .option('--talker <name>', '只看某个会话(wxid 或昵称)')
7447
+ .option('--limit <number>', '每天最多列几条', '6')
7448
+ .option('--json', '输出 JSON 格式')
7449
+ .action(async (opts) => {
7450
+ const { execFile } = await import('child_process')
7451
+ const { promisify } = await import('util')
7452
+ const execFileAsync = promisify(execFile)
7453
+ const script = join(resolvePackageRoot(), 'scripts', 'on_this_day.py')
7454
+ const limit = parseCliInteger(opts.limit, 'limit', 1, 200, opts.json)
7455
+
7456
+ try {
7457
+ const args = [script, '--years', String(opts.years || '1,3,5'), '--limit', String(limit)]
7458
+ if (opts.talker) args.push('--talker', String(opts.talker))
7459
+ if (opts.json) args.push('--json')
7460
+ const { stdout } = await execFileAsync(getPythonCommand(), args, {
7461
+ timeout: 300_000,
7462
+ maxBuffer: 10 * 1024 * 1024,
7463
+ env: pythonProcessEnv(),
7464
+ })
7465
+ console.log(stdout)
7466
+ } catch (e: any) {
7467
+ if (opts.json) {
7468
+ console.log(JSON.stringify({
7469
+ success: false,
7470
+ code: 'TODAY_FAILED',
7471
+ error: '那年今日不可用,请运行 check --json 检查配置和依赖',
7472
+ }))
7473
+ process.exit(1)
7474
+ }
7475
+ console.error(chalk.red(`\n${safeSubprocessError(e, '回溯失败')}`))
7476
+ process.exit(1)
7477
+ }
7478
+ })
7479
+
7480
+ // ==================== network(人脉图谱)====================
7481
+ program
7482
+ .command('network')
7483
+ .description('人脉图谱:共同群、谁能做引荐人(只读 contact.db,不碰聊天内容)')
7484
+ .option('--groups', '列群概览(成员数)')
7485
+ .option('--with <name>', '我和他共同在哪些群')
7486
+ .option('--intro <name>', '谁适合把我介绍给他')
7487
+ .option('--json', '输出 JSON 格式')
7488
+ .action(async (opts) => {
7489
+ const { execFile } = await import('child_process')
7490
+ const { promisify } = await import('util')
7491
+ const execFileAsync = promisify(execFile)
7492
+ const script = join(resolvePackageRoot(), 'scripts', 'network.py')
7493
+
7494
+ try {
7495
+ const args = [script]
7496
+ if (opts.groups) args.push('--groups')
7497
+ if (opts.with) args.push('--with', String(opts.with))
7498
+ if (opts.intro) args.push('--intro', String(opts.intro))
7499
+ if (opts.json) args.push('--json')
7500
+ if (args.length === 1) {
7501
+ console.error(chalk.yellow('\n用法:network --groups | --with <名字> | --intro <名字>'))
7502
+ process.exit(1)
7503
+ }
7504
+ const { stdout } = await execFileAsync(getPythonCommand(), args, {
7505
+ timeout: 300_000,
7506
+ maxBuffer: 10 * 1024 * 1024,
7507
+ env: pythonProcessEnv(),
7508
+ })
7509
+ console.log(stdout)
7510
+ } catch (e: any) {
7511
+ if (opts.json) {
7512
+ console.log(JSON.stringify({
7513
+ success: false,
7514
+ code: 'NETWORK_FAILED',
7515
+ error: '人脉图谱不可用,请运行 check --json 检查配置和依赖',
7516
+ }))
7517
+ process.exit(1)
7518
+ }
7519
+ console.error(chalk.red(`\n${safeSubprocessError(e, '查询失败')}`))
7520
+ process.exit(1)
7521
+ }
7522
+ })
7523
+
7524
+ // ==================== promises(欠账清单)====================
7525
+ program
7526
+ .command('promises')
7527
+ .description('欠账清单:我说过要给谁做什么、后来做了没有(规则判定,带原话与后续证据)')
7528
+ .option('--days <number>', '看最近多少天', '60')
7529
+ .option('--talker <name>', '只看某个会话')
7530
+ .option('--limit <number>', '最多列几条', '30')
7531
+ .option('--json', '输出 JSON 格式')
7532
+ .action(async (opts) => {
7533
+ const { execFile } = await import('child_process')
7534
+ const { promisify } = await import('util')
7535
+ const execFileAsync = promisify(execFile)
7536
+ const script = join(resolvePackageRoot(), 'scripts', 'promises.py')
7537
+ const days = parseCliInteger(opts.days, 'days', 1, 3650, opts.json)
7538
+ const limit = parseCliInteger(opts.limit, 'limit', 1, 500, opts.json)
7539
+
7540
+ try {
7541
+ const args = [script, '--days', String(days), '--limit', String(limit)]
7542
+ if (opts.talker) args.push('--talker', String(opts.talker))
7543
+ if (opts.json) args.push('--json')
7544
+ const { stdout } = await execFileAsync(getPythonCommand(), args, {
7545
+ timeout: 300_000,
7546
+ maxBuffer: 10 * 1024 * 1024,
7547
+ env: pythonProcessEnv(),
7548
+ })
7549
+ console.log(stdout)
7550
+ } catch (e: any) {
7551
+ if (opts.json) {
7552
+ console.log(JSON.stringify({
7553
+ success: false,
7554
+ code: 'PROMISES_FAILED',
7555
+ error: '欠账清单不可用,请运行 check --json 检查配置和依赖',
7556
+ }))
7557
+ process.exit(1)
7558
+ }
7559
+ console.error(chalk.red(`\n${safeSubprocessError(e, '统计失败')}`))
7560
+ process.exit(1)
7561
+ }
7562
+ })
7563
+
7564
+ // ==================== timeline(某号观点演变)====================
7565
+ program
7566
+ .command('timeline')
7567
+ .description('观点时间线:某个公众号历次文章与要点,可按时间看它说法的变化')
7568
+ .option('--source <name>', '公众号名(子串匹配)')
7569
+ .option('--list', '列出存了哪些号、各多少篇')
7570
+ .option('--limit <number>', '最多列几篇(最近的在前)', '30')
7571
+ .option('--ai', '让模型总结观点变化(需要 API key)')
7572
+ .option('--json', '输出 JSON 格式')
7573
+ .action(async (opts) => {
7574
+ const { execFile } = await import('child_process')
7575
+ const { promisify } = await import('util')
7576
+ const execFileAsync = promisify(execFile)
7577
+ const script = join(resolvePackageRoot(), 'scripts', 'opinion_timeline.py')
7578
+ const limit = parseCliInteger(opts.limit, 'limit', 1, 2000, opts.json)
7579
+
7580
+ try {
7581
+ const args = [script, '--limit', String(limit)]
7582
+ if (opts.list) args.push('--list')
7583
+ if (opts.source) args.push('--source', String(opts.source))
7584
+ if (opts.ai) args.push('--ai')
7585
+ if (opts.json) args.push('--json')
7586
+ if (!opts.list && !opts.source) {
7587
+ console.error(chalk.yellow('\n用法:timeline --list | --source <公众号名>'))
7588
+ process.exit(1)
7589
+ }
7590
+ const { stdout } = await execFileAsync(getPythonCommand(), args, {
7591
+ timeout: 300_000,
7592
+ maxBuffer: 10 * 1024 * 1024,
7593
+ env: pythonProcessEnv(),
7594
+ })
7595
+ console.log(stdout)
7596
+ } catch (e: any) {
7597
+ if (opts.json) {
7598
+ console.log(JSON.stringify({
7599
+ success: false,
7600
+ code: 'TIMELINE_FAILED',
7601
+ error: '观点时间线不可用,请先运行 daily 生成日报',
7602
+ }))
7603
+ process.exit(1)
7604
+ }
7605
+ console.error(chalk.red(`\n${safeSubprocessError(e, '读取失败')}`))
7606
+ process.exit(1)
7607
+ }
7608
+ })
7609
+
7441
7610
  // ==================== daily-server ====================
7442
7611
  program
7443
7612
  .command('daily-server')
@@ -7721,6 +7721,183 @@ program
7721
7721
  process.exit(1);
7722
7722
  }
7723
7723
  });
7724
+ // ==================== today(那年今日)====================
7725
+ program
7726
+ .command('today')
7727
+ .description('那年今日:几年前的今天你在聊什么(只读本地库,不联网)')
7728
+ .option('--years <list>', '往前看几年,逗号分隔', '1,3,5')
7729
+ .option('--talker <name>', '只看某个会话(wxid 或昵称)')
7730
+ .option('--limit <number>', '每天最多列几条', '6')
7731
+ .option('--json', '输出 JSON 格式')
7732
+ .action(async (opts) => {
7733
+ const { execFile } = await import('child_process');
7734
+ const { promisify } = await import('util');
7735
+ const execFileAsync = promisify(execFile);
7736
+ const script = join(resolvePackageRoot(), 'scripts', 'on_this_day.py');
7737
+ const limit = parseCliInteger(opts.limit, 'limit', 1, 200, opts.json);
7738
+ try {
7739
+ const args = [script, '--years', String(opts.years || '1,3,5'), '--limit', String(limit)];
7740
+ if (opts.talker)
7741
+ args.push('--talker', String(opts.talker));
7742
+ if (opts.json)
7743
+ args.push('--json');
7744
+ const { stdout } = await execFileAsync(getPythonCommand(), args, {
7745
+ timeout: 300_000,
7746
+ maxBuffer: 10 * 1024 * 1024,
7747
+ env: pythonProcessEnv(),
7748
+ });
7749
+ console.log(stdout);
7750
+ }
7751
+ catch (e) {
7752
+ if (opts.json) {
7753
+ console.log(JSON.stringify({
7754
+ success: false,
7755
+ code: 'TODAY_FAILED',
7756
+ error: '那年今日不可用,请运行 check --json 检查配置和依赖',
7757
+ }));
7758
+ process.exit(1);
7759
+ }
7760
+ console.error(chalk.red(`\n${safeSubprocessError(e, '回溯失败')}`));
7761
+ process.exit(1);
7762
+ }
7763
+ });
7764
+ // ==================== network(人脉图谱)====================
7765
+ program
7766
+ .command('network')
7767
+ .description('人脉图谱:共同群、谁能做引荐人(只读 contact.db,不碰聊天内容)')
7768
+ .option('--groups', '列群概览(成员数)')
7769
+ .option('--with <name>', '我和他共同在哪些群')
7770
+ .option('--intro <name>', '谁适合把我介绍给他')
7771
+ .option('--json', '输出 JSON 格式')
7772
+ .action(async (opts) => {
7773
+ const { execFile } = await import('child_process');
7774
+ const { promisify } = await import('util');
7775
+ const execFileAsync = promisify(execFile);
7776
+ const script = join(resolvePackageRoot(), 'scripts', 'network.py');
7777
+ try {
7778
+ const args = [script];
7779
+ if (opts.groups)
7780
+ args.push('--groups');
7781
+ if (opts.with)
7782
+ args.push('--with', String(opts.with));
7783
+ if (opts.intro)
7784
+ args.push('--intro', String(opts.intro));
7785
+ if (opts.json)
7786
+ args.push('--json');
7787
+ if (args.length === 1) {
7788
+ console.error(chalk.yellow('\n用法:network --groups | --with <名字> | --intro <名字>'));
7789
+ process.exit(1);
7790
+ }
7791
+ const { stdout } = await execFileAsync(getPythonCommand(), args, {
7792
+ timeout: 300_000,
7793
+ maxBuffer: 10 * 1024 * 1024,
7794
+ env: pythonProcessEnv(),
7795
+ });
7796
+ console.log(stdout);
7797
+ }
7798
+ catch (e) {
7799
+ if (opts.json) {
7800
+ console.log(JSON.stringify({
7801
+ success: false,
7802
+ code: 'NETWORK_FAILED',
7803
+ error: '人脉图谱不可用,请运行 check --json 检查配置和依赖',
7804
+ }));
7805
+ process.exit(1);
7806
+ }
7807
+ console.error(chalk.red(`\n${safeSubprocessError(e, '查询失败')}`));
7808
+ process.exit(1);
7809
+ }
7810
+ });
7811
+ // ==================== promises(欠账清单)====================
7812
+ program
7813
+ .command('promises')
7814
+ .description('欠账清单:我说过要给谁做什么、后来做了没有(规则判定,带原话与后续证据)')
7815
+ .option('--days <number>', '看最近多少天', '60')
7816
+ .option('--talker <name>', '只看某个会话')
7817
+ .option('--limit <number>', '最多列几条', '30')
7818
+ .option('--json', '输出 JSON 格式')
7819
+ .action(async (opts) => {
7820
+ const { execFile } = await import('child_process');
7821
+ const { promisify } = await import('util');
7822
+ const execFileAsync = promisify(execFile);
7823
+ const script = join(resolvePackageRoot(), 'scripts', 'promises.py');
7824
+ const days = parseCliInteger(opts.days, 'days', 1, 3650, opts.json);
7825
+ const limit = parseCliInteger(opts.limit, 'limit', 1, 500, opts.json);
7826
+ try {
7827
+ const args = [script, '--days', String(days), '--limit', String(limit)];
7828
+ if (opts.talker)
7829
+ args.push('--talker', String(opts.talker));
7830
+ if (opts.json)
7831
+ args.push('--json');
7832
+ const { stdout } = await execFileAsync(getPythonCommand(), args, {
7833
+ timeout: 300_000,
7834
+ maxBuffer: 10 * 1024 * 1024,
7835
+ env: pythonProcessEnv(),
7836
+ });
7837
+ console.log(stdout);
7838
+ }
7839
+ catch (e) {
7840
+ if (opts.json) {
7841
+ console.log(JSON.stringify({
7842
+ success: false,
7843
+ code: 'PROMISES_FAILED',
7844
+ error: '欠账清单不可用,请运行 check --json 检查配置和依赖',
7845
+ }));
7846
+ process.exit(1);
7847
+ }
7848
+ console.error(chalk.red(`\n${safeSubprocessError(e, '统计失败')}`));
7849
+ process.exit(1);
7850
+ }
7851
+ });
7852
+ // ==================== timeline(某号观点演变)====================
7853
+ program
7854
+ .command('timeline')
7855
+ .description('观点时间线:某个公众号历次文章与要点,可按时间看它说法的变化')
7856
+ .option('--source <name>', '公众号名(子串匹配)')
7857
+ .option('--list', '列出存了哪些号、各多少篇')
7858
+ .option('--limit <number>', '最多列几篇(最近的在前)', '30')
7859
+ .option('--ai', '让模型总结观点变化(需要 API key)')
7860
+ .option('--json', '输出 JSON 格式')
7861
+ .action(async (opts) => {
7862
+ const { execFile } = await import('child_process');
7863
+ const { promisify } = await import('util');
7864
+ const execFileAsync = promisify(execFile);
7865
+ const script = join(resolvePackageRoot(), 'scripts', 'opinion_timeline.py');
7866
+ const limit = parseCliInteger(opts.limit, 'limit', 1, 2000, opts.json);
7867
+ try {
7868
+ const args = [script, '--limit', String(limit)];
7869
+ if (opts.list)
7870
+ args.push('--list');
7871
+ if (opts.source)
7872
+ args.push('--source', String(opts.source));
7873
+ if (opts.ai)
7874
+ args.push('--ai');
7875
+ if (opts.json)
7876
+ args.push('--json');
7877
+ if (!opts.list && !opts.source) {
7878
+ console.error(chalk.yellow('\n用法:timeline --list | --source <公众号名>'));
7879
+ process.exit(1);
7880
+ }
7881
+ const { stdout } = await execFileAsync(getPythonCommand(), args, {
7882
+ timeout: 300_000,
7883
+ maxBuffer: 10 * 1024 * 1024,
7884
+ env: pythonProcessEnv(),
7885
+ });
7886
+ console.log(stdout);
7887
+ }
7888
+ catch (e) {
7889
+ if (opts.json) {
7890
+ console.log(JSON.stringify({
7891
+ success: false,
7892
+ code: 'TIMELINE_FAILED',
7893
+ error: '观点时间线不可用,请先运行 daily 生成日报',
7894
+ }));
7895
+ process.exit(1);
7896
+ }
7897
+ console.error(chalk.red(`\n${safeSubprocessError(e, '读取失败')}`));
7898
+ process.exit(1);
7899
+ }
7900
+ });
7724
7901
  // ==================== daily-server ====================
7725
7902
  program
7726
7903
  .command('daily-server')