weflow-cli 1.15.0 → 1.16.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/CHANGELOG.md CHANGED
@@ -17,7 +17,23 @@ 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
20
+ ## 1.16.0
21
+
22
+ - **`leads` - the offers and asks buried in chat, with the sentence that said them.** Scans recent
23
+ conversations for five signal families (contact details, price, purchase, partnership, a request)
24
+ and ranks them, because the ones worth answering combine "how much" with "here is my number". Every
25
+ hit keeps the original words and whether you have replied yet (没回 / 我回过 / 已跟进 / 已结束).
26
+ Rule-based and offline on purpose: the output is a shortlist to read, not a verdict.
27
+ Two filters had to exist before it was usable - the first version returned eight "leads" in thirty
28
+ days and **every one was a campus-recruiting poster or a shopping promo**, because those are exactly
29
+ the messages salted with 合作 / 采购 / 报价. Adverts are now dropped, a signal must sit inside a
30
+ question or a request, and `@mentions` plus URLs are stripped before matching (group nicknames often
31
+ contain a phone number, and a URL with a long numeric id reads as one).
32
+ - **`bonds` now shows the last thing said in a cooling conversation.** "已静默 304 天" does not tell
33
+ you whether to reach out; the final line does. That was the missing half of a "who should I contact"
34
+ reminder - the ranking and the silence were already computed.
35
+
36
+ ## 1.15.1
21
37
 
22
38
  - **Four read-only answers to questions WeChat cannot answer.** `weflow-cli today` shows what you were
23
39
  talking about on this day in earlier years (it walks every shard and every conversation table, so
package/OPERATIONS.md CHANGED
@@ -1095,7 +1095,16 @@ powershell -NoProfile -ExecutionPolicy Bypass -File <仓库>\scripts\install_tas
1095
1095
  下游(AI 报告、统计)静默少一半。
1096
1096
 
1097
1097
  动作只有一句"跑那个脚本",具体的事在仓库里的 `scripts/daily_task.ps1`(纯 ASCII、路径全部相对自身)。
1098
- 第一个位置参数是模式(`noon` = 今天;缺省 = 昨天)。它做四件事,每件都有理由:
1098
+ 第一个位置参数是模式(`noon` = 今天;缺省 = 昨天)。
1099
+
1100
+ > **动作是 `wscript.exe //B //Nologo run_hidden.vbs [noon]`,不是直接跑 PowerShell。**
1101
+ > `powershell.exe` 是**控制台程序**:计划任务一触发,Windows 会先给它建控制台窗口,之后 PowerShell
1102
+ > 才轮到隐藏自己 —— 结果就是每次运行都闪出一个黑框,全屏游戏时更明显(2026-10-22 实测:00:05 那次
1103
+ > 弹窗被人顺手关掉,进程以 `0xC000013A`(Ctrl+C 类中断)退出,那天就没有日报也没有邮件)。
1104
+ > `wscript.exe` 是没有控制台的 GUI 进程,`run_hidden.vbs` 内部再用 `WshShell.Run(cmd, 0, True)`
1105
+ > 隐藏子进程并等待它结束 —— 全程零窗口,而 `True` 让任务的"上次运行结果"仍然是真实退出码。
1106
+
1107
+ 它做五件事,每件都有理由:
1099
1108
 
1100
1109
  1. **跑目标天**:日终 `pipeline.py --yesterday --prefetch-bodies`;午报 `pipeline.py --date <今天> --prefetch-bodies`。
1101
1110
  2. **把输出落盘**:`logs/daily-<日期>[-noon].log` 与 `daily-<日期>[-noon].err.log`(`logs/` 已 gitignore)。
@@ -1110,6 +1119,12 @@ powershell -NoProfile -ExecutionPolicy Bypass -File <仓库>\scripts\install_tas
1110
1119
  午报也会写 index.html,只看它会让 00:05 跳过下午、留下一份"看着完整、实际半天"的日报。
1111
1120
  实际判据是 `.articles.json` 的 `generated_at`:**生成于目标日之后**(通常就是次日 00:0x),
1112
1121
  或当晚 23:00 之后,才算这天做完了。
1122
+ 5. **渲染失败单独重试**:`pipeline.py` 的最后一环是渲染 `index.html`,而这一步已经挂过两次
1123
+ (2026-10-18 无输出 11 小时;2026-10-23 在 README 之后中断,日报只剩 13:00 的半天版)。
1124
+ 抓取和摘要那时已经在 `.articles.json` 里,所以脚本会**单独**再跑一次 `generate_html.py`;
1125
+ 它成功就把这次运行当作成功,索引与邮件照常继续。判据是 `.articles.json` 比 `index.html` 新
1126
+ (或页面不存在)——页面比数据新说明这一轮已经渲染过了,不重复。
1127
+ **没有这一步,渲染慢一次就等于早上没有邮件**,因为发信挂在"整天成功"上。
1113
1128
 
1114
1129
  > 为什么不把 python 命令直接写进任务动作:① 实测 `Register-ScheduledTask` 对带嵌套引号的
1115
1130
  > `-Argument` 会报 `0x80004005`,而包一层脚本就不用跟引号搏斗;② 任务动作指向 CLI 那个 npm 包时,
package/README.en.md CHANGED
@@ -19,7 +19,9 @@
19
19
 
20
20
  </div>
21
21
 
22
- **WeFlow CLI turns WeChat into your local second brain**: read and query chat history locally with one command, export readable HTML; turn official-account articles into AI-digested daily reports; read your WeChat favorites; build a semantic-searchable knowledge base — and plug it all into MCP-compatible AI editors like Claude Code. Works on Windows and Linux.
22
+ **WeFlow CLI turns WeChat into your local second brain**: read and query chat history locally with one command, export readable HTML; turn official-account articles into AI-digested daily reports (optionally mailed to you); read your WeChat favorites; build a semantic-searchable knowledge base — voice messages included — and plug it all into MCP-compatible AI editors like Claude Code. Works on Windows and Linux.
23
+
24
+ It is built to answer **questions WeChat itself cannot**: who is waiting on a reply, what I promised someone and whether I followed through, which relationships are cooling, how an account's position shifted over time, and what we were talking about on this day in earlier years.
23
25
 
24
26
  > 🔒 **Local-first**: runs entirely on your machine — zero telemetry, zero cloud reporting. Database keys are stored locally, encrypted with machine-bound AES-256-GCM. AI features only activate after you explicitly configure your own API key. See the [Security & Privacy statement](./SECURITY.md).
25
27
 
@@ -60,6 +62,17 @@ weflow-cli chat "How does RAG work?" # knowledge-base RAG Q&A
60
62
  weflow-cli mcp-config # one-shot MCP client integration
61
63
  ```
62
64
 
65
+ **Questions WeChat cannot answer** (all read-only, no network, no model calls):
66
+
67
+ ```text
68
+ weflow-cli awaiting # who is waiting on a reply
69
+ weflow-cli bonds # relationship temperature: cooling, warming, WeChat-only
70
+ weflow-cli promises --days 180 # what I said I'd do, with the original words and follow-up evidence
71
+ weflow-cli today --years 1,3,5 # what we were talking about on this day in earlier years
72
+ weflow-cli network --with "contact" # shared groups; --intro finds who could introduce you
73
+ weflow-cli timeline --source "account" # one account's articles laid out in time
74
+ ```
75
+
63
76
  ## Platform Support
64
77
 
65
78
  | Capability | Windows | macOS | Linux |
@@ -283,6 +296,10 @@ Management: `weflow-cli assistant status` / `log` / `stop`; send "帮助" in WeC
283
296
  | Who is waiting on a reply | `weflow-cli awaiting --dry-run` · `weflow-cli awaiting --yes` |
284
297
  | Draft a reply (text unless you pick and send) | `weflow-cli draft <contact> --dry-run` · `--yes` (**only** `--pick N --send --yes` actually sends; both flags are required) |
285
298
  | Relationship temperature | `weflow-cli bonds` (who is cooling, who is warming, who only has WeChat; read-only, local, no model) |
299
+ | What I promised whom | `weflow-cli promises --days 180` (original words plus follow-up evidence; rule-based, no model) |
300
+ | This day in earlier years | `weflow-cli today --years 1,3,5` (what you were talking about back then) |
301
+ | People graph | `weflow-cli network --groups` · `--with <name>` (shared groups) · `--intro <name>` (who could introduce you; reads `contact.db` only) |
302
+ | One account over time | `weflow-cli timeline --list` · `--source <account>` (its articles laid out in time) |
286
303
  | Local judgement primitive | `weflow-cli decide --request <file> --dry-run` · `--yes` |
287
304
  | Export chat history | `weflow-cli export <contact> <json\|txt\|html\|excel>` |
288
305
  | Sync checkpoint | `weflow-cli sync run <contact> --since <date>` · `sync status` · `sync verify` |
@@ -290,7 +307,7 @@ Management: `weflow-cli assistant status` / `log` / `stop`; send "帮助" in WeC
290
307
  | Moments cache | `weflow-cli sns timeline` · `weflow-cli sns users` · `weflow-cli sns stats` |
291
308
  | WeChat favorites | `weflow-cli fav list` · `weflow-cli fav export markdown` · `weflow-cli fav set-key` |
292
309
  | WeRead | `weflow-cli weread shelf` · `notes` · `search` · `stats` (get the key from https://weread.qq.com/r/weread-skills; see [OPERATIONS.md](./OPERATIONS.md)) |
293
- | Knowledge base | `weflow-cli vault` · `weflow-cli wiki` · `weflow-cli search <query>` · `weflow-cli chat` (semantic search needs `config set dashscopeApiKey` first) |
310
+ | Knowledge base | `weflow-cli vault` · `weflow-cli wiki` · `weflow-cli search <query>` · `weflow-cli chat` (semantic search needs `config set dashscopeApiKey` first; `search --phrase <words> --line all` also matches voice transcripts) |
294
311
  | Reports & tasks | `weflow-cli report` · `annual-report` · `todos` |
295
312
  | Second-brain assistant | `weflow-cli assistant start` · `status` · `log` · `stop` |
296
313
  | On-device panel | `weflow-cli panel` — floating window; `--status` reads state only, `--ask "…"` asks from the command line |
@@ -356,6 +373,8 @@ For a more complete view of modules and data flow, read [ARCHITECTURE.md](./ARCH
356
373
  - After upgrading WeChat, switching accounts, or migrating computers, you may need to re-initialize or re-scan NT keys.
357
374
  - `init` validates existing local configuration first and does not re-capture keys when that validation passes. Use `weflow-cli init --refresh` only after a migration, an account switch, or an access failure.
358
375
  - To test a first-time initialization or a failed key, prefer `weflow-cli init --test-missing-keys`; it simulates missing keys for that run only and leaves the saved configuration untouched. When keys really have failed, `weflow-cli config forget-keys` clears the database access keys only and asks for confirmation.
376
+ - **Voice transcription runs on your machine** (`faster-whisper`, local model); audio never leaves it, and transcripts are cached by content hash so an interrupted run resumes. It is **not guaranteed accurate** — dialect, accent and background noise all introduce errors; the purpose is "you can find what was said", not a verbatim record. Transcripts join the phrase index only, never the semantic index (no second embedding bill for dialect-approximate text).
377
+ - **Message shards are split by time** (`message_0..3.db`, roughly a year each), so "older chats" depend on whether the shard covering that period is still on the machine; `weflow-cli check --report` lists each shard's read status.
359
378
  - Running `weflow-cli daily` without a date checks yesterday's artifacts first: if yesterday's `README.md`, article index, or `index.html` is missing, it backfills yesterday and only generates today once that succeeds. Passing `--date` or `--dry-run` skips the backfill.
360
379
 
361
380
  ## Documentation & Feedback
package/README.md CHANGED
@@ -19,7 +19,9 @@
19
19
 
20
20
  </div>
21
21
 
22
- **WeFlow CLI 把微信变成你的本地第二大脑**:在本机读取和查询聊天记录、导出可读 HTML;把公众号文章整理成带 AI 摘要的日报;读取微信收藏;构建可语义搜索的知识库;并通过 MCP 接入 Claude Code 等 AI 编辑器。支持 Windows 与 Linux。
22
+ **WeFlow CLI 把微信变成你的本地第二大脑**:在本机读取和查询聊天记录、导出可读 HTML;把公众号文章整理成带 AI 摘要的日报(可邮件推送);读取微信收藏;构建可语义搜索的知识库(连语音消息也能搜);并通过 MCP 接入 Claude Code 等 AI 编辑器。支持 Windows 与 Linux。
23
+
24
+ 它专门回答**几个微信自己回答不了的问题**:谁在等我回话、我答应过谁什么后来做了没有、和谁的关系在变淡、某个号的观点怎么变的、那一年的今天我们在聊什么。
23
25
 
24
26
  > 🔒 **本地优先**:完全在你的电脑上运行——零遥测、零云上报;数据库密钥以机器绑定的 AES-256-GCM 加密存储在本机。AI 功能仅在显式配置你自己的 API Key 后启用。详见[安全与隐私声明](./SECURITY.md)。
25
27
 
@@ -60,6 +62,17 @@ weflow-cli chat "RAG 的原理是什么?" # 知识库 RAG 问答
60
62
  weflow-cli mcp-config # 一键接入 Claude Code 等 MCP 客户端
61
63
  ```
62
64
 
65
+ **几个微信给不出答案的问题**(都只读本地库,不联网、不调模型):
66
+
67
+ ```text
68
+ weflow-cli awaiting # 谁在等我回话
69
+ weflow-cli bonds # 关系温度:谁在冷却、谁只有微信这一条线
70
+ weflow-cli promises --days 180 # 我答应过谁什么,后来做了没有(带原话与后续证据)
71
+ weflow-cli today --years 1,3,5 # 那几年的今天,我们在聊什么
72
+ weflow-cli network --with "联系人A" # 我和他共同在哪些群;--intro 看谁能引荐
73
+ weflow-cli timeline --source "某号" # 这个号历次说法的时间线
74
+ ```
75
+
63
76
  ## 平台支持
64
77
 
65
78
  | 能力 | Windows | macOS | Linux |
@@ -297,6 +310,7 @@ weflow-cli config set assistantGroupRequireMention true
297
310
  | 帮我起草回复(默认只出文字) | `weflow-cli draft <联系人> --dry-run` · `--yes`(**只有** `--pick N --send --yes` 会真的替你发出去;两步缺一不可)|
298
311
  | 关系温度 | `weflow-cli bonds`(谁在冷却、谁在变热、谁只有微信这一条线;只读本地库,不联网、不调模型)|
299
312
  | 我说过要给谁做什么 | `weflow-cli promises --days 180`(带回原话与后续证据,规则判定,不调模型)|
313
+ | 聊天里的生意线索 | `weflow-cli leads --days 60 --min-score 4`(报价/采购/合作/需求信号,带原话与"我回过没有")|
300
314
  | 那年今日 | `weflow-cli today --years 1,3,5`(几年前的今天在聊什么)|
301
315
  | 人脉图谱 | `weflow-cli network --groups` · `--with <名字>`(共同群)· `--intro <名字>`(谁能引荐;只读 contact.db)|
302
316
  | 某号观点演变 | `weflow-cli timeline --list` · `--source <公众号名>`(按时间看它说法的变化)|
@@ -307,7 +321,7 @@ weflow-cli config set assistantGroupRequireMention true
307
321
  | 朋友圈缓存 | `weflow-cli sns timeline` · `weflow-cli sns users` · `weflow-cli sns stats` |
308
322
  | 微信收藏 | `weflow-cli fav list` · `weflow-cli fav export markdown` · `weflow-cli fav set-key` |
309
323
  | 微信读书 | `weflow-cli weread shelf` · `notes` · `search` · `stats`(key 从 https://weread.qq.com/r/weread-skills 拿,见 [OPERATIONS.md](./OPERATIONS.md)) |
310
- | 知识库 | `weflow-cli vault` · `weflow-cli wiki` · `weflow-cli search <query>` · `weflow-cli chat`(语义检索需先 `config set dashscopeApiKey`)|
324
+ | 知识库 | `weflow-cli vault` · `weflow-cli wiki` · `weflow-cli search <query>` · `weflow-cli chat`(语义检索需先 `config set dashscopeApiKey`;`search --phrase <词> --line all` 连语音转写一起搜)|
311
325
  | 总结与任务 | `weflow-cli report` · `annual-report` · `todos` |
312
326
  | 第二大脑助手 | `weflow-cli assistant start` · `status` · `log` · `stop` |
313
327
  | 本机面板 | `weflow-cli panel`(悬浮窗;`--status` 只看状态,`--ask "…"` 在命令行里问)|
@@ -374,6 +388,8 @@ python scripts/fav_server.py --date YYYY-MM-DD
374
388
  - `init` 会先验证已有本地配置;验证通过时不再重复捕获密钥。仅在迁移、切换账号或访问失败后使用 `weflow-cli init --refresh`。
375
389
  - 测试首次初始化或密钥失效时,优先使用 `weflow-cli init --test-missing-keys`;它只在本次运行模拟密钥缺失,不改动已保存配置。实际密钥失效时可使用 `weflow-cli config forget-keys`,该命令仅清除数据库访问密钥并要求确认。
376
390
  - 不指定日期运行 `weflow-cli daily` 时,会先检查昨天的日报产物;昨天缺少 `README.md`、文章索引或 `index.html` 时,会先补齐昨天,成功后才生成今天。指定 `--date` 或使用 `--dry-run` 时不执行补日报。
391
+ - 语音转写用**本机** Whisper 模型(`faster-whisper`),音频不出本机;转写按内容哈希缓存,可中断续跑。它**不保证准确**——方言、口音和背景音都会带来误差,用途是"能搜到说过什么",不是逐字记录。转写结果只并入短语索引,不进语义索引(不为方言近似文本再付一次嵌入费用)。
392
+ - 消息分片按**时间段**切分(`message_0..3.db` 各覆盖一年多),所以"更早的聊天"取决于那段时间对应的分片是否还在本机;`weflow-cli check --report` 会列出每个分片的读取状态。
377
393
 
378
394
  ## 文档与反馈
379
395
 
package/bin/weflow-cli.ts CHANGED
@@ -7561,6 +7561,47 @@ program
7561
7561
  }
7562
7562
  })
7563
7563
 
7564
+ // ==================== leads(线索雷达)====================
7565
+ program
7566
+ .command('leads')
7567
+ .description('线索雷达:聊天里的报价/采购/合作/需求信号,带原话与跟进状态(规则判定,不调模型)')
7568
+ .option('--days <number>', '看最近多少天', '30')
7569
+ .option('--min-score <number>', '最低分,4 分以上更接近真实生意', '2')
7570
+ .option('--limit <number>', '最多列几条', '20')
7571
+ .option('--json', '输出 JSON 格式')
7572
+ .action(async (opts) => {
7573
+ const { execFile } = await import('child_process')
7574
+ const { promisify } = await import('util')
7575
+ const execFileAsync = promisify(execFile)
7576
+ const script = join(resolvePackageRoot(), 'scripts', 'leads.py')
7577
+ const days = parseCliInteger(opts.days, 'days', 1, 3650, opts.json)
7578
+ const minScore = parseCliInteger(opts.minScore, 'min-score', 1, 50, opts.json)
7579
+ const limit = parseCliInteger(opts.limit, 'limit', 1, 500, opts.json)
7580
+
7581
+ try {
7582
+ const args = [script, '--days', String(days), '--min-score', String(minScore),
7583
+ '--limit', String(limit)]
7584
+ if (opts.json) args.push('--json')
7585
+ const { stdout } = await execFileAsync(getPythonCommand(), args, {
7586
+ timeout: 300_000,
7587
+ maxBuffer: 10 * 1024 * 1024,
7588
+ env: pythonProcessEnv(),
7589
+ })
7590
+ console.log(stdout)
7591
+ } catch (e: any) {
7592
+ if (opts.json) {
7593
+ console.log(JSON.stringify({
7594
+ success: false,
7595
+ code: 'LEADS_FAILED',
7596
+ error: '线索雷达不可用,请运行 check --json 检查配置和依赖',
7597
+ }))
7598
+ process.exit(1)
7599
+ }
7600
+ console.error(chalk.red(`\n${safeSubprocessError(e, '统计失败')}`))
7601
+ process.exit(1)
7602
+ }
7603
+ })
7604
+
7564
7605
  // ==================== timeline(某号观点演变)====================
7565
7606
  program
7566
7607
  .command('timeline')
@@ -7849,6 +7849,47 @@ program
7849
7849
  process.exit(1);
7850
7850
  }
7851
7851
  });
7852
+ // ==================== leads(线索雷达)====================
7853
+ program
7854
+ .command('leads')
7855
+ .description('线索雷达:聊天里的报价/采购/合作/需求信号,带原话与跟进状态(规则判定,不调模型)')
7856
+ .option('--days <number>', '看最近多少天', '30')
7857
+ .option('--min-score <number>', '最低分,4 分以上更接近真实生意', '2')
7858
+ .option('--limit <number>', '最多列几条', '20')
7859
+ .option('--json', '输出 JSON 格式')
7860
+ .action(async (opts) => {
7861
+ const { execFile } = await import('child_process');
7862
+ const { promisify } = await import('util');
7863
+ const execFileAsync = promisify(execFile);
7864
+ const script = join(resolvePackageRoot(), 'scripts', 'leads.py');
7865
+ const days = parseCliInteger(opts.days, 'days', 1, 3650, opts.json);
7866
+ const minScore = parseCliInteger(opts.minScore, 'min-score', 1, 50, opts.json);
7867
+ const limit = parseCliInteger(opts.limit, 'limit', 1, 500, opts.json);
7868
+ try {
7869
+ const args = [script, '--days', String(days), '--min-score', String(minScore),
7870
+ '--limit', String(limit)];
7871
+ if (opts.json)
7872
+ args.push('--json');
7873
+ const { stdout } = await execFileAsync(getPythonCommand(), args, {
7874
+ timeout: 300_000,
7875
+ maxBuffer: 10 * 1024 * 1024,
7876
+ env: pythonProcessEnv(),
7877
+ });
7878
+ console.log(stdout);
7879
+ }
7880
+ catch (e) {
7881
+ if (opts.json) {
7882
+ console.log(JSON.stringify({
7883
+ success: false,
7884
+ code: 'LEADS_FAILED',
7885
+ error: '线索雷达不可用,请运行 check --json 检查配置和依赖',
7886
+ }));
7887
+ process.exit(1);
7888
+ }
7889
+ console.error(chalk.red(`\n${safeSubprocessError(e, '统计失败')}`));
7890
+ process.exit(1);
7891
+ }
7892
+ });
7852
7893
  // ==================== timeline(某号观点演变)====================
7853
7894
  program
7854
7895
  .command('timeline')