weflow-cli 1.13.1 → 1.14.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,44 @@ 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.14.1
21
+
22
+ - **The daily run now keeps the retrieval indexes fresh.** On 2026-10-22 the semantic index was
23
+ still at 10-15 and the phrase index at 10-17, so `search` could not find anything newer than a
24
+ week ago - nothing had ever refreshed them. After the day's report is written, the wrapper runs
25
+ `semantic_search.py update` (embeds only what is missing: 4,061 new records on that first run)
26
+ and `semantic_search.py phrase-build` (local FTS, no API cost). Each has its own log and neither
27
+ can fail the day.
28
+ - **The noon pass mails too, and says it is half a day.** It used to generate silently, which read
29
+ as "it never ran" - the only evidence was a log file. The noon email is titled
30
+ 「…(截至中午)」 and states that the complete version arrives the next morning, so the two daily
31
+ emails cannot be mistaken for duplicates.
32
+ - **`phrase-build` no longer fails when `VACUUM` cannot run.** `VACUUM` needs a temp file, and in a
33
+ restricted environment it reports `unable to open database file` *after* the index has been
34
+ written successfully - that turned a good rebuild into exit code 1.
35
+
36
+ ## 1.14.0
37
+
38
+ - **A daily report you can receive by mail, cut down to the topics you actually read.**
39
+ `daily --email` mails one report with two sections - AI and 学术 - dropping 新闻 / 政治 / 投资 /
40
+ 文学, and each section opens with an AI-written digest over that section's titles. Every article
41
+ keeps the one-line analysis `biz_daily` already produced (no extra calls), its source and time,
42
+ its link, and any GitHub repository mentioned in the body (14 of the 110 articles on 2026-10-18).
43
+ Recipient comes from `--mail-to`, then config `dailyMailTo`, then the SMTP account itself.
44
+ - **Each topic is clustered again before it is printed.** 68 AI articles in one list is a wall;
45
+ an AI pass groups them into 4-6 named sections (模型与产品 / 智能体与工程 / 科研工具 …), each with a
46
+ coloured dot. Tags were not usable: 68 articles carried 382 distinct tags, the most frequent
47
+ appearing 6 times. A degenerate answer (everything in one group) is retried with a stricter
48
+ instruction, because it silently collapses the layout back to a single wall - observed on
49
+ 2026-10-21, where 91 articles ended up in one group.
50
+ - **Uniform cards, and no colour without a label.** Every article is the same card; only the badge
51
+ colour and summary length differ, so a section never looks like two layouts mixed together.
52
+ Relevance is a small tag that *says* 高 instead of a colour the reader has to guess.
53
+ - **The scheduled run mails the report once a day.** `scripts/daily_task.ps1` calls `daily_mail.py`
54
+ only after the day pass produced the report - the noon pass generates but does not mail, so the
55
+ inbox gets one complete day instead of a half day followed by a repeat. A mail failure is logged
56
+ separately and does not turn a successful day into a failed one.
57
+
20
58
  ## 1.13.1
21
59
 
22
60
  - **Fix: a data directory containing non-ASCII characters was decoded wrong, so `check --report`
package/bin/weflow-cli.ts CHANGED
@@ -1169,13 +1169,13 @@ const configurableKeys = [
1169
1169
  'ntDbPath', 'ntKey', 'ntSalt', 'contactDbPath', 'contactKey', 'contactSalt',
1170
1170
  'vaultRepo', 'aiEngine', 'aiBaseUrl', 'aiModel', 'deepseekApiKey', 'typesafeApiKey',
1171
1171
  'dashscopeApiKey', 'favPassphrase',
1172
- 'judgeProvider', 'openrouterApiKey', 'openrouterJudgeModel',
1172
+ 'judgeProvider', 'openrouterApiKey', 'openrouterJudgeModel', 'judgeLocalUrl',
1173
1173
  'wereadApiKey',
1174
1174
  'assistantPrivacy', 'assistantWhitelist', 'assistantGroupWhitelist',
1175
1175
  'assistantGroupRequireMention', 'assistantFastRoute', 'assistantPanelUser', 'quickReplyContacts',
1176
1176
  'skillDirs', 'skillDisabled',
1177
1177
  'dailySources', 'dailySourceCategories',
1178
- 'dailyExcludeTopics', 'dailyAiEnabled',
1178
+ 'dailyExcludeTopics', 'dailyAiEnabled', 'dailyMailTo',
1179
1179
  'wechatMediaDownload',
1180
1180
  'emoticonSeed',
1181
1181
  ] as const
@@ -6359,12 +6359,15 @@ program
6359
6359
  .option('--no-summary', '只判断不生成:不调 LLM 写摘要/标签/简报(主题与相关度仍由 Jev 判断),无需 DeepSeek key')
6360
6360
  .option('--prefetch-bodies [n]', '抓取之前先并发把当天正文抓进缓存(n=并发数,默认 8):抓取阶段于是全部命中缓存、不再逐篇 sleep 8-12 秒(450 篇约 75 分钟 → 秒级)。对上游的请求次数不变,只是压进更短的时间窗')
6361
6361
  .option('--json', '输出机器可读的最终结果;运行日志写入 stderr')
6362
+ .option('--email', '生成后把日报按主题(AI / 文学 / 学术)发到邮箱(每个主题附一段 AI 综述,逐篇给要点)')
6363
+ .option('--mail-to <address>', '邮件收件人;缺省用配置 dailyMailTo,再缺省发给自己')
6362
6364
  .action(async (opts) => {
6363
6365
  const { spawn } = await import('child_process')
6364
6366
  const { existsSync, statSync } = await import('fs')
6365
6367
  const pkgRoot = resolvePackageRoot()
6366
6368
  const pipeline = join(pkgRoot, 'scripts', 'pipeline.py')
6367
6369
  const bizDaily = join(pkgRoot, 'scripts', 'biz_daily.py')
6370
+ const dailyMail = join(pkgRoot, 'scripts', 'daily_mail.py')
6368
6371
 
6369
6372
  const now = new Date()
6370
6373
  const localDate = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`
@@ -6489,6 +6492,24 @@ program
6489
6492
  const code = await runPipeline(date)
6490
6493
  if (code === 0) {
6491
6494
  const complete = isComplete(date)
6495
+ // 邮件只依赖 `.articles.json`(逐篇摘要已经由 biz_daily 写好),所以放在生成之后、
6496
+ // 与"是否完整"无关:不完整时也先把已有的发出去,用户不至于什么都收不到。
6497
+ let mail: { ok: boolean; code: number } | null = null
6498
+ if (opts.email) {
6499
+ const mailArgs = [dailyMail, '--date', date]
6500
+ if (opts.mailTo) mailArgs.push('--to', String(opts.mailTo))
6501
+ dailyLog(chalk.cyan('\n📧 正在按主题整理并发邮件...'))
6502
+ mail = await new Promise((resolve) => {
6503
+ const child = spawn(getPythonCommand(), mailArgs, {
6504
+ windowsHide: true,
6505
+ stdio: 'inherit',
6506
+ env: pythonProcessEnv(apiKey),
6507
+ })
6508
+ child.on('error', () => resolve({ ok: false, code: 1 }))
6509
+ child.on('exit', (c) => resolve({ ok: (c || 0) === 0, code: c || 0 }))
6510
+ })
6511
+ if (mail && !mail.ok) dailyLog(chalk.yellow(' ! 邮件没发出去(日报本身已生成,见上面的报错)'))
6512
+ }
6492
6513
  if (opts.json) {
6493
6514
  console.log(JSON.stringify({
6494
6515
  success: complete,
@@ -6497,11 +6518,13 @@ program
6497
6518
  aiEnabled: !noAi,
6498
6519
  output: `output/biz-daily/${date}`,
6499
6520
  artifacts: artifactStatus(date),
6521
+ mail,
6500
6522
  }, null, 2))
6501
6523
  if (!complete) process.exit(1)
6502
6524
  return
6503
6525
  }
6504
6526
  console.log(chalk.green(`\n✓ 日报已生成: output/biz-daily/${date}/`))
6527
+ if (mail) console.log(chalk.green(`✓ 邮件已发送${opts.mailTo ? ` → ${opts.mailTo}` : ''}`))
6505
6528
  console.log(chalk.gray(` HTML 阅读器: weflow-cli daily-server --date ${date}`))
6506
6529
  } else {
6507
6530
  if (opts.json) console.log(JSON.stringify({ success: false, code: 'DAILY_PIPELINE_FAILED', date, aiEnabled: !noAi }))
@@ -1141,13 +1141,13 @@ const configurableKeys = [
1141
1141
  'ntDbPath', 'ntKey', 'ntSalt', 'contactDbPath', 'contactKey', 'contactSalt',
1142
1142
  'vaultRepo', 'aiEngine', 'aiBaseUrl', 'aiModel', 'deepseekApiKey', 'typesafeApiKey',
1143
1143
  'dashscopeApiKey', 'favPassphrase',
1144
- 'judgeProvider', 'openrouterApiKey', 'openrouterJudgeModel',
1144
+ 'judgeProvider', 'openrouterApiKey', 'openrouterJudgeModel', 'judgeLocalUrl',
1145
1145
  'wereadApiKey',
1146
1146
  'assistantPrivacy', 'assistantWhitelist', 'assistantGroupWhitelist',
1147
1147
  'assistantGroupRequireMention', 'assistantFastRoute', 'assistantPanelUser', 'quickReplyContacts',
1148
1148
  'skillDirs', 'skillDisabled',
1149
1149
  'dailySources', 'dailySourceCategories',
1150
- 'dailyExcludeTopics', 'dailyAiEnabled',
1150
+ 'dailyExcludeTopics', 'dailyAiEnabled', 'dailyMailTo',
1151
1151
  'wechatMediaDownload',
1152
1152
  'emoticonSeed',
1153
1153
  ];
@@ -6627,12 +6627,15 @@ program
6627
6627
  .option('--no-summary', '只判断不生成:不调 LLM 写摘要/标签/简报(主题与相关度仍由 Jev 判断),无需 DeepSeek key')
6628
6628
  .option('--prefetch-bodies [n]', '抓取之前先并发把当天正文抓进缓存(n=并发数,默认 8):抓取阶段于是全部命中缓存、不再逐篇 sleep 8-12 秒(450 篇约 75 分钟 → 秒级)。对上游的请求次数不变,只是压进更短的时间窗')
6629
6629
  .option('--json', '输出机器可读的最终结果;运行日志写入 stderr')
6630
+ .option('--email', '生成后把日报按主题(AI / 文学 / 学术)发到邮箱(每个主题附一段 AI 综述,逐篇给要点)')
6631
+ .option('--mail-to <address>', '邮件收件人;缺省用配置 dailyMailTo,再缺省发给自己')
6630
6632
  .action(async (opts) => {
6631
6633
  const { spawn } = await import('child_process');
6632
6634
  const { existsSync, statSync } = await import('fs');
6633
6635
  const pkgRoot = resolvePackageRoot();
6634
6636
  const pipeline = join(pkgRoot, 'scripts', 'pipeline.py');
6635
6637
  const bizDaily = join(pkgRoot, 'scripts', 'biz_daily.py');
6638
+ const dailyMail = join(pkgRoot, 'scripts', 'daily_mail.py');
6636
6639
  const now = new Date();
6637
6640
  const localDate = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`;
6638
6641
  const isoDate = (when) => `${when.getFullYear()}-${String(when.getMonth() + 1).padStart(2, '0')}-${String(when.getDate()).padStart(2, '0')}`;
@@ -6761,6 +6764,26 @@ program
6761
6764
  const code = await runPipeline(date);
6762
6765
  if (code === 0) {
6763
6766
  const complete = isComplete(date);
6767
+ // 邮件只依赖 `.articles.json`(逐篇摘要已经由 biz_daily 写好),所以放在生成之后、
6768
+ // 与"是否完整"无关:不完整时也先把已有的发出去,用户不至于什么都收不到。
6769
+ let mail = null;
6770
+ if (opts.email) {
6771
+ const mailArgs = [dailyMail, '--date', date];
6772
+ if (opts.mailTo)
6773
+ mailArgs.push('--to', String(opts.mailTo));
6774
+ dailyLog(chalk.cyan('\n📧 正在按主题整理并发邮件...'));
6775
+ mail = await new Promise((resolve) => {
6776
+ const child = spawn(getPythonCommand(), mailArgs, {
6777
+ windowsHide: true,
6778
+ stdio: 'inherit',
6779
+ env: pythonProcessEnv(apiKey),
6780
+ });
6781
+ child.on('error', () => resolve({ ok: false, code: 1 }));
6782
+ child.on('exit', (c) => resolve({ ok: (c || 0) === 0, code: c || 0 }));
6783
+ });
6784
+ if (mail && !mail.ok)
6785
+ dailyLog(chalk.yellow(' ! 邮件没发出去(日报本身已生成,见上面的报错)'));
6786
+ }
6764
6787
  if (opts.json) {
6765
6788
  console.log(JSON.stringify({
6766
6789
  success: complete,
@@ -6769,12 +6792,15 @@ program
6769
6792
  aiEnabled: !noAi,
6770
6793
  output: `output/biz-daily/${date}`,
6771
6794
  artifacts: artifactStatus(date),
6795
+ mail,
6772
6796
  }, null, 2));
6773
6797
  if (!complete)
6774
6798
  process.exit(1);
6775
6799
  return;
6776
6800
  }
6777
6801
  console.log(chalk.green(`\n✓ 日报已生成: output/biz-daily/${date}/`));
6802
+ if (mail)
6803
+ console.log(chalk.green(`✓ 邮件已发送${opts.mailTo ? ` → ${opts.mailTo}` : ''}`));
6778
6804
  console.log(chalk.gray(` HTML 阅读器: weflow-cli daily-server --date ${date}`));
6779
6805
  }
6780
6806
  else {