weflow-cli 1.16.0 → 1.17.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 +42 -0
- package/README.md +16 -4
- package/bin/weflow-cli.ts +348 -6
- package/dist/bin/weflow-cli.js +381 -8
- package/dist/bin/weflow-cli.js.map +1 -1
- package/dist/src/core/ntCore.d.ts +47 -0
- package/dist/src/core/ntCore.d.ts.map +1 -1
- package/dist/src/core/ntCore.js +24 -0
- package/dist/src/core/ntCore.js.map +1 -1
- package/dist/src/services/assistantService.d.ts +8 -1
- package/dist/src/services/assistantService.d.ts.map +1 -1
- package/dist/src/services/assistantService.js +47 -8
- package/dist/src/services/assistantService.js.map +1 -1
- package/dist/src/services/assistantTools.d.ts +8 -0
- package/dist/src/services/assistantTools.d.ts.map +1 -1
- package/dist/src/services/assistantTools.js +67 -11
- package/dist/src/services/assistantTools.js.map +1 -1
- 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/cursorProbe.d.ts +44 -0
- package/dist/src/services/cursorProbe.d.ts.map +1 -0
- package/dist/src/services/cursorProbe.js +183 -0
- package/dist/src/services/cursorProbe.js.map +1 -0
- package/dist/src/services/qchatImport.d.ts +89 -0
- package/dist/src/services/qchatImport.d.ts.map +1 -0
- package/dist/src/services/qchatImport.js +273 -0
- package/dist/src/services/qchatImport.js.map +1 -0
- package/dist/src/services/setupVenv.d.ts +43 -0
- package/dist/src/services/setupVenv.d.ts.map +1 -0
- package/dist/src/services/setupVenv.js +198 -0
- package/dist/src/services/setupVenv.js.map +1 -0
- package/dist/src/utils/python.d.ts +10 -0
- package/dist/src/utils/python.d.ts.map +1 -1
- package/dist/src/utils/python.js +27 -4
- package/dist/src/utils/python.js.map +1 -1
- package/docs/DATA_CONTRACT.md +25 -0
- package/docs/DECISIONS.md +92 -30
- package/docs/FUNCTIONAL-IMPROVEMENT-SPEC.md +365 -365
- package/docs/HEALTH-CHECK.md +104 -104
- package/docs/JEV/345/272/224/347/224/250/346/226/275/345/267/245/346/226/207/346/241/243.md +310 -310
- package/docs/NEXT_ACTIONS.md +236 -236
- package/docs/NPM-PUBLISH.md +130 -130
- package/docs/POSITIONING.md +68 -68
- package/docs/PROJECT_STATE.md +6 -3
- package/docs/REPLAY.md +169 -0
- package/docs/SYNC_CONTRACT.md +197 -180
- package/package.json +89 -88
- package/requirements-3x.txt +4 -4
- package/requirements-voice.txt +15 -15
- package/resources/js/graph3d/NOTICE.txt +35 -35
- package/resources/js/graph3d/d3-binarytree.min.js +2 -2
- package/resources/js/graph3d/d3-dispatch.min.js +2 -2
- package/resources/js/graph3d/d3-force-3d.min.js +2 -2
- package/resources/js/graph3d/d3-octree.min.js +2 -2
- package/resources/js/graph3d/d3-quadtree.min.js +2 -2
- package/resources/js/graph3d/d3-timer.min.js +2 -2
- package/resources/js/graph3d/three.min.js +6 -6
- package/resources/js/graph3d/viewer2d.js +550 -448
- package/resources/panel/quick-menu.cjs +197 -197
- package/scripts/_utils.py +1338 -1338
- package/scripts/article_notes.py +423 -423
- package/scripts/chat_notes.py +86 -3
- package/scripts/check_readme_commands.py +427 -426
- package/scripts/contact_schema.py +339 -339
- package/scripts/create_reading_notes.py +11 -11
- package/scripts/daily_task.ps1 +213 -213
- package/scripts/fav_notes.py +321 -321
- package/scripts/graph_2d.py +156 -151
- package/scripts/graph_3d.py +672 -646
- package/scripts/health_check.py +473 -473
- package/scripts/imported_chats.py +68 -0
- package/scripts/install_tasks.ps1 +105 -105
- package/scripts/leads.py +179 -179
- package/scripts/network.py +206 -206
- package/scripts/nt_decrypt.py +135 -1
- package/scripts/on_this_day.py +178 -178
- package/scripts/opinion_timeline.py +138 -138
- package/scripts/promises.py +185 -185
- package/scripts/replay.py +803 -0
- package/scripts/run_hidden.vbs +25 -0
- package/scripts/semantic_search.py +73 -5
- package/scripts/user_notes.py +315 -315
- package/scripts/voice_transcribe.py +139 -139
- package/scripts/watch_issues.py +212 -212
- package/scripts/watch_mail.py +131 -131
- package/src/core/ntCore.ts +59 -0
- package/src/services/assistantService.ts +48 -8
- package/src/services/assistantTools.ts +73 -10
- package/src/services/configService.ts +3 -2
- package/src/services/cursorProbe.ts +222 -0
- package/src/services/qchatImport.ts +327 -0
- package/src/services/setupVenv.ts +238 -0
- package/src/services/syncService.ts +363 -363
- package/src/services/syncState.ts +275 -275
- package/src/services/wechatMedia.ts +166 -166
- package/src/utils/python.ts +33 -4
|
@@ -480,21 +480,21 @@ def main():
|
|
|
480
480
|
parser.add_argument('--date', help='日期 YYYY-MM-DD(--refresh-queries 时不需要)')
|
|
481
481
|
parser.add_argument('--source', default=DEFAULT_SOURCE, help='文章目录')
|
|
482
482
|
parser.add_argument('--vault', default=DEFAULT_VAULT, help='Vault 目录')
|
|
483
|
-
parser.add_argument('--clean-summaries', action='store_true',
|
|
484
|
-
help='只把已有笔记摘要段里的微信界面残留洗掉(本地,不调模型)')
|
|
483
|
+
parser.add_argument('--clean-summaries', action='store_true',
|
|
484
|
+
help='只把已有笔记摘要段里的微信界面残留洗掉(本地,不调模型)')
|
|
485
485
|
parser.add_argument('--refresh-queries', action='store_true',
|
|
486
486
|
help='只做这件事:把已有笔记里的 dataview 查询重写成当前模板(本地,不调模型)')
|
|
487
487
|
args = parser.parse_args()
|
|
488
488
|
|
|
489
|
-
if args.clean_summaries:
|
|
490
|
-
# 本地清洗:动的是本地就能算出来的东西,不必把两万多篇重问一遍
|
|
491
|
-
result = clean_summaries(args.vault)
|
|
492
|
-
print('清洗摘要:%d 篇;本来就干净、没动的 %d 篇;读不出来的 %d 篇'
|
|
493
|
-
% (len(result['cleaned']), len(result['untouched']), len(result['skipped'])))
|
|
494
|
-
for name in result['skipped'][:5]:
|
|
495
|
-
print(' 跳过:', name[:70])
|
|
496
|
-
return
|
|
497
|
-
|
|
489
|
+
if args.clean_summaries:
|
|
490
|
+
# 本地清洗:动的是本地就能算出来的东西,不必把两万多篇重问一遍
|
|
491
|
+
result = clean_summaries(args.vault)
|
|
492
|
+
print('清洗摘要:%d 篇;本来就干净、没动的 %d 篇;读不出来的 %d 篇'
|
|
493
|
+
% (len(result['cleaned']), len(result['untouched']), len(result['skipped'])))
|
|
494
|
+
for name in result['skipped'][:5]:
|
|
495
|
+
print(' 跳过:', name[:70])
|
|
496
|
+
return
|
|
497
|
+
|
|
498
498
|
if args.refresh_queries:
|
|
499
499
|
result = refresh_dataview_blocks(args.vault)
|
|
500
500
|
print('重写 %d 篇;没有查询块、没动的 %d 篇;读不出来的 %d 篇'
|
package/scripts/daily_task.ps1
CHANGED
|
@@ -1,213 +1,213 @@
|
|
|
1
|
-
# Runs a daily report pass, with everything written to logs/daily-<date>.log.
|
|
2
|
-
#
|
|
3
|
-
# Two modes, because a WeChat official-account day only fills up gradually:
|
|
4
|
-
#
|
|
5
|
-
# daily_task.ps1 noon -> today so far (run at 13:00; half a day of push messages)
|
|
6
|
-
# daily_task.ps1 -> yesterday, complete (run at 00:05; same as before)
|
|
7
|
-
#
|
|
8
|
-
# The second pass is incremental by design: `.run_state.json` makes biz_daily skip the
|
|
9
|
-
# articles the noon pass already handled, so 00:05 only fetches/summarises what arrived
|
|
10
|
-
# in the afternoon, while README/index.html are rebuilt from every md on disk.
|
|
11
|
-
#
|
|
12
|
-
# Why this wrapper exists instead of putting the command straight into the scheduled task:
|
|
13
|
-
#
|
|
14
|
-
# 1. The task must not depend on a build artifact. It calls the python layer
|
|
15
|
-
# (scripts/pipeline.py) directly, which is the same source the repo builds from.
|
|
16
|
-
# 2. A task whose output goes nowhere answers "it failed" and nothing else. One log
|
|
17
|
-
# file per run, so "why was yesterday short?" has an answer.
|
|
18
|
-
# 3. Redirecting stdout to a FILE changes python's stdout encoding (it stops being the
|
|
19
|
-
# console and becomes the locale codepage). A title containing a character outside
|
|
20
|
-
# that codepage would then raise UnicodeEncodeError *after* the day was already
|
|
21
|
-
# written, i.e. the run looks like a failure while the artifacts are fine. Hence
|
|
22
|
-
# PYTHONIOENCODING/PYTHONUTF8 below.
|
|
23
|
-
#
|
|
24
|
-
# Extra arguments are appended, so `daily_task.ps1 --help` is a safe smoke test (it
|
|
25
|
-
# prints the pipeline's help into that day's log and exits without touching anything).
|
|
26
|
-
$ErrorActionPreference = 'Stop'
|
|
27
|
-
|
|
28
|
-
$root = Split-Path -Parent $PSScriptRoot
|
|
29
|
-
$logDir = Join-Path $root 'logs'
|
|
30
|
-
if (-not (Test-Path $logDir)) { New-Item -ItemType Directory -Path $logDir | Out-Null }
|
|
31
|
-
|
|
32
|
-
# Mode is the first positional word; anything else is forwarded to pipeline.py.
|
|
33
|
-
$mode = 'day'
|
|
34
|
-
$rest = @($args)
|
|
35
|
-
if ($args.Count -gt 0 -and ($args[0] -eq 'noon' -or $args[0] -eq 'day')) {
|
|
36
|
-
$mode = $args[0]
|
|
37
|
-
$rest = if ($args.Count -gt 1) { @($args[1..($args.Count - 1)]) } else { @() }
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
$today = (Get-Date).ToString('yyyy-MM-dd')
|
|
41
|
-
$yesterday = (Get-Date).AddDays(-1).ToString('yyyy-MM-dd')
|
|
42
|
-
$targetDay = if ($mode -eq 'noon') { $today } else { $yesterday }
|
|
43
|
-
|
|
44
|
-
$stamp = Get-Date -Format 'yyyy-MM-dd'
|
|
45
|
-
$suffix = if ($mode -eq 'noon') { '-noon' } else { '' }
|
|
46
|
-
$out = Join-Path $logDir ('daily-{0}{1}.log' -f $stamp, $suffix)
|
|
47
|
-
$err = Join-Path $logDir ('daily-{0}{1}.err.log' -f $stamp, $suffix)
|
|
48
|
-
|
|
49
|
-
$env:PYTHONIOENCODING = 'utf-8'
|
|
50
|
-
$env:PYTHONUTF8 = '1'
|
|
51
|
-
|
|
52
|
-
$python = (Get-Command python.exe).Source
|
|
53
|
-
$script = Join-Path $PSScriptRoot 'pipeline.py'
|
|
54
|
-
$modeArgs = if ($mode -eq 'noon') { @('--date', $today) } else { @('--yesterday') }
|
|
55
|
-
$allArgs = @($script) + $modeArgs + @('--prefetch-bodies') + $rest
|
|
56
|
-
|
|
57
|
-
# Idempotent "make sure the target day is as complete as it can be right now", not
|
|
58
|
-
# "run at 08:20".
|
|
59
|
-
#
|
|
60
|
-
# Why: this machine slept from 2026-10-17 02:09 to 2026-10-18 15:26 and BOTH 08:20
|
|
61
|
-
# triggers were counted as missed while StartWhenAvailable=On did not start the task
|
|
62
|
-
# on wake (NumberOfMissedRuns=2, no run). A schedule that only fires if the machine
|
|
63
|
-
# happens to be awake at one instant is not a schedule. With the guard below the task
|
|
64
|
-
# can be triggered cheaply and often (schedule + at logon): if the target is already
|
|
65
|
-
# done this exits in milliseconds, otherwise it produces/extends the day.
|
|
66
|
-
#
|
|
67
|
-
# The day-mode guard deliberately does NOT trust index.html alone: the noon pass also
|
|
68
|
-
# writes one, so "index.html exists" would make the 00:05 pass skip the afternoon and
|
|
69
|
-
# leave the day half-done behind a complete-looking page. `.articles.json`'s
|
|
70
|
-
# generated_at tells us when the last pass actually ran; anything during that day
|
|
71
|
-
# (before 23:00) is a mid-day artifact and must be extended.
|
|
72
|
-
#
|
|
73
|
-
# Note the day pass runs at 00:05 **the next day**, so a finished day is one whose
|
|
74
|
-
# generated_at is later than the day itself - not one stamped at 23:xx. Testing
|
|
75
|
-
# `hour -ge 23` alone would have made every 00:05 run redo the whole day (observed
|
|
76
|
-
# while verifying this guard: generated_at 00:20 was judged incomplete).
|
|
77
|
-
function Test-DayComplete([string]$dir, [string]$targetDay) {
|
|
78
|
-
$idx = Join-Path $dir 'index.html'
|
|
79
|
-
if (-not (Test-Path $idx)) { return $false }
|
|
80
|
-
$art = Join-Path $dir '.articles.json'
|
|
81
|
-
if (-not (Test-Path $art)) { return $true } # old artifact without json: trust it
|
|
82
|
-
try {
|
|
83
|
-
$j = Get-Content $art -Raw -Encoding UTF8 | ConvertFrom-Json
|
|
84
|
-
$gen = [datetime]::ParseExact([string]$j.generated_at, 'yyyy-MM-dd HH:mm:ss', $null)
|
|
85
|
-
$genDay = $gen.ToString('yyyy-MM-dd')
|
|
86
|
-
# finished = generated on a later day (the usual 00:05 pass), or late the same night
|
|
87
|
-
return (($genDay -gt $targetDay) -or (($genDay -eq $targetDay) -and ($gen.Hour -ge 23)))
|
|
88
|
-
} catch { return $true }
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
# Skipped when arguments are passed, so `daily_task.ps1 --help` still works as a
|
|
92
|
-
# smoke test with no side effects.
|
|
93
|
-
if ($rest.Count -eq 0) {
|
|
94
|
-
$dayDir = Join-Path $root ('output\biz-daily\' + $targetDay)
|
|
95
|
-
$done = if ($mode -eq 'noon') { Test-Path (Join-Path $dayDir '.noon_done') } else { Test-DayComplete $dayDir $targetDay }
|
|
96
|
-
if ($done) {
|
|
97
|
-
$note = '===== {0} [{1}] {2} already complete - skip =====' -f `
|
|
98
|
-
(Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $mode, $targetDay
|
|
99
|
-
Add-Content -Path (Join-Path $logDir 'daily-task.log') -Value $note -Encoding UTF8
|
|
100
|
-
exit 0
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
# Start-Process OVERWRITES its redirect targets, so a same-day rerun would erase the log
|
|
105
|
-
# of the run that just failed - exactly the one worth keeping. Rotate any existing pair to
|
|
106
|
-
# daily-<date>.run<N>[.err].log first, so every attempt stays on disk. N is the first free
|
|
107
|
-
# index, which also works when several reruns happen in one day.
|
|
108
|
-
if (Test-Path $out) {
|
|
109
|
-
$n = 1
|
|
110
|
-
while (Test-Path (Join-Path $logDir ('daily-{0}{1}.run{2}.log' -f $stamp, $suffix, $n))) { $n++ }
|
|
111
|
-
Move-Item -Path $out -Destination (Join-Path $logDir ('daily-{0}{1}.run{2}.log' -f $stamp, $suffix, $n))
|
|
112
|
-
if (Test-Path $err) {
|
|
113
|
-
Move-Item -Path $err -Destination (Join-Path $logDir ('daily-{0}{1}.run{2}.err.log' -f $stamp, $suffix, $n))
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
# Start-Process writes the raw bytes through: no PowerShell error-record wrapping, and
|
|
118
|
-
# no shell quoting to get wrong.
|
|
119
|
-
$proc = Start-Process -FilePath $python -ArgumentList $allArgs -NoNewWindow -Wait -PassThru `
|
|
120
|
-
-RedirectStandardOutput $out -RedirectStandardError $err
|
|
121
|
-
|
|
122
|
-
# Fallback for the HTML stage.
|
|
123
|
-
#
|
|
124
|
-
# `pipeline.py` fetches, summarises, writes the day, then renders `index.html`. That last step
|
|
125
|
-
# has hung twice now (2026-10-18: 11 hours with no output; 2026-10-23: the run died after the
|
|
126
|
-
# README and never produced the page). Losing the page also loses the mail, because mailing
|
|
127
|
-
# happens only on success - so a single slow step silently costs the whole morning report.
|
|
128
|
-
#
|
|
129
|
-
# The fetch and summaries are already on disk at that point (`.articles.json`), so rendering is
|
|
130
|
-
# retried on its own. `$proc` is replaced by the retry's exit code, which is what lets the mail
|
|
131
|
-
# and index steps below run on a day where only the renderer failed.
|
|
132
|
-
if ($proc.ExitCode -ne 0) {
|
|
133
|
-
$dayDir = Join-Path $root ('output\biz-daily\' + $targetDay)
|
|
134
|
-
$art = Join-Path $dayDir '.articles.json'
|
|
135
|
-
$idx = Join-Path $dayDir 'index.html'
|
|
136
|
-
$htmlScript = Join-Path $PSScriptRoot 'generate_html.py'
|
|
137
|
-
# Only retry when the data is there and the page is missing or older than the data:
|
|
138
|
-
# a page newer than `.articles.json` means rendering already happened this run.
|
|
139
|
-
$needsRender = (Test-Path $art) -and (Test-Path $htmlScript)
|
|
140
|
-
if ($needsRender -and (Test-Path $idx)) {
|
|
141
|
-
$needsRender = (Get-Item $art).LastWriteTime -gt (Get-Item $idx).LastWriteTime
|
|
142
|
-
}
|
|
143
|
-
if ($needsRender) {
|
|
144
|
-
Add-Content -Path (Join-Path $logDir 'daily-task.log') -Value `
|
|
145
|
-
('===== {0} [{1}] {2} pipeline exit {3}, retrying HTML only =====' -f `
|
|
146
|
-
(Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $mode, $targetDay, $proc.ExitCode) -Encoding UTF8
|
|
147
|
-
$retryProc = Start-Process -FilePath $python `
|
|
148
|
-
-ArgumentList @($htmlScript, '--date', $targetDay) `
|
|
149
|
-
-NoNewWindow -Wait -PassThru `
|
|
150
|
-
-RedirectStandardOutput (Join-Path $logDir ('html-retry-{0}{1}.log' -f $stamp, $suffix)) `
|
|
151
|
-
-RedirectStandardError (Join-Path $logDir ('html-retry-{0}{1}.err.log' -f $stamp, $suffix))
|
|
152
|
-
if ($retryProc.ExitCode -eq 0) {
|
|
153
|
-
$proc = $retryProc
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
if ($proc.ExitCode -eq 0 -and $rest.Count -eq 0) {
|
|
159
|
-
# Marker for the noon pass only; the day pass is judged by generated_at above.
|
|
160
|
-
if ($mode -eq 'noon') {
|
|
161
|
-
$dayDir = Join-Path $root ('output\biz-daily\' + $targetDay)
|
|
162
|
-
if (Test-Path $dayDir) {
|
|
163
|
-
Set-Content -Path (Join-Path $dayDir '.noon_done') -Value (Get-Date -Format 'yyyy-MM-dd HH:mm:ss') -Encoding UTF8
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
# Retrieval indexes lag behind unless something refreshes them: on 2026-10-22 the
|
|
168
|
-
# semantic index was still at 10-15 (7 days stale) and the phrase index at 10-17, so
|
|
169
|
-
# `search` simply could not find anything newer. Both run here, after the day is on
|
|
170
|
-
# disk, in their own logs -- a failure is recorded and never fails the day.
|
|
171
|
-
$searchScript = Join-Path $PSScriptRoot 'semantic_search.py'
|
|
172
|
-
if ($mode -eq 'day' -and (Test-Path $searchScript)) {
|
|
173
|
-
$idxOut = Join-Path $logDir ('index-{0}.log' -f $stamp)
|
|
174
|
-
$idxErr = Join-Path $logDir ('index-{0}.err.log' -f $stamp)
|
|
175
|
-
# `update` only embeds what is not in the index yet (4,061 new records on the first
|
|
176
|
-
# run after a week). `phrase-build` is local FTS, no API, so it rebuilds in full.
|
|
177
|
-
$idxProc = Start-Process -FilePath $python `
|
|
178
|
-
-ArgumentList @($searchScript, 'update', '--article-days', '30') `
|
|
179
|
-
-NoNewWindow -Wait -PassThru -RedirectStandardOutput $idxOut -RedirectStandardError $idxErr
|
|
180
|
-
$phraseProc = Start-Process -FilePath $python `
|
|
181
|
-
-ArgumentList @($searchScript, 'phrase-build') `
|
|
182
|
-
-NoNewWindow -Wait -PassThru `
|
|
183
|
-
-RedirectStandardOutput (Join-Path $logDir ('phrase-{0}.log' -f $stamp)) `
|
|
184
|
-
-RedirectStandardError (Join-Path $logDir ('phrase-{0}.err.log' -f $stamp))
|
|
185
|
-
if ($idxProc.ExitCode -ne 0 -or $phraseProc.ExitCode -ne 0) {
|
|
186
|
-
Add-Content -Path (Join-Path $logDir 'daily-task.log') -Value `
|
|
187
|
-
('===== {0} index update failed (update={1}, phrase={2}), see {3} =====' -f `
|
|
188
|
-
(Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $idxProc.ExitCode, $phraseProc.ExitCode, $idxErr) -Encoding UTF8
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
# **Both passes mail now.** The noon pass used to be silent, which read as "it never
|
|
193
|
-
# ran" - the only way to tell it had done anything was to open the log. The two emails
|
|
194
|
-
# are clearly different: the noon one is labelled 截至中午 and says the complete version
|
|
195
|
-
# follows next morning. Mail is still its own step with its own log pair, so a mail
|
|
196
|
-
# failure never fails the day nor blocks the noon marker.
|
|
197
|
-
$mailScript = Join-Path $PSScriptRoot 'daily_mail.py'
|
|
198
|
-
$mailOut = Join-Path $logDir ('daily-mail-{0}{1}.log' -f $stamp, $suffix)
|
|
199
|
-
$mailErr = Join-Path $logDir ('daily-mail-{0}{1}.err.log' -f $stamp, $suffix)
|
|
200
|
-
if (Test-Path $mailScript) {
|
|
201
|
-
$mailArgs = @($mailScript, '--date', $targetDay)
|
|
202
|
-
if ($mode -eq 'noon') { $mailArgs += @('--label', '截至中午') }
|
|
203
|
-
$mailProc = Start-Process -FilePath $python -ArgumentList $mailArgs `
|
|
204
|
-
-NoNewWindow -Wait -PassThru -RedirectStandardOutput $mailOut -RedirectStandardError $mailErr
|
|
205
|
-
if ($mailProc.ExitCode -ne 0) {
|
|
206
|
-
Add-Content -Path (Join-Path $logDir 'daily-task.log') -Value `
|
|
207
|
-
('===== {0} [{1}] {2} mail failed (exit {3}), see {4} =====' -f `
|
|
208
|
-
(Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $mode, $targetDay, $mailProc.ExitCode, $mailErr) -Encoding UTF8
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
exit $proc.ExitCode
|
|
1
|
+
# Runs a daily report pass, with everything written to logs/daily-<date>.log.
|
|
2
|
+
#
|
|
3
|
+
# Two modes, because a WeChat official-account day only fills up gradually:
|
|
4
|
+
#
|
|
5
|
+
# daily_task.ps1 noon -> today so far (run at 13:00; half a day of push messages)
|
|
6
|
+
# daily_task.ps1 -> yesterday, complete (run at 00:05; same as before)
|
|
7
|
+
#
|
|
8
|
+
# The second pass is incremental by design: `.run_state.json` makes biz_daily skip the
|
|
9
|
+
# articles the noon pass already handled, so 00:05 only fetches/summarises what arrived
|
|
10
|
+
# in the afternoon, while README/index.html are rebuilt from every md on disk.
|
|
11
|
+
#
|
|
12
|
+
# Why this wrapper exists instead of putting the command straight into the scheduled task:
|
|
13
|
+
#
|
|
14
|
+
# 1. The task must not depend on a build artifact. It calls the python layer
|
|
15
|
+
# (scripts/pipeline.py) directly, which is the same source the repo builds from.
|
|
16
|
+
# 2. A task whose output goes nowhere answers "it failed" and nothing else. One log
|
|
17
|
+
# file per run, so "why was yesterday short?" has an answer.
|
|
18
|
+
# 3. Redirecting stdout to a FILE changes python's stdout encoding (it stops being the
|
|
19
|
+
# console and becomes the locale codepage). A title containing a character outside
|
|
20
|
+
# that codepage would then raise UnicodeEncodeError *after* the day was already
|
|
21
|
+
# written, i.e. the run looks like a failure while the artifacts are fine. Hence
|
|
22
|
+
# PYTHONIOENCODING/PYTHONUTF8 below.
|
|
23
|
+
#
|
|
24
|
+
# Extra arguments are appended, so `daily_task.ps1 --help` is a safe smoke test (it
|
|
25
|
+
# prints the pipeline's help into that day's log and exits without touching anything).
|
|
26
|
+
$ErrorActionPreference = 'Stop'
|
|
27
|
+
|
|
28
|
+
$root = Split-Path -Parent $PSScriptRoot
|
|
29
|
+
$logDir = Join-Path $root 'logs'
|
|
30
|
+
if (-not (Test-Path $logDir)) { New-Item -ItemType Directory -Path $logDir | Out-Null }
|
|
31
|
+
|
|
32
|
+
# Mode is the first positional word; anything else is forwarded to pipeline.py.
|
|
33
|
+
$mode = 'day'
|
|
34
|
+
$rest = @($args)
|
|
35
|
+
if ($args.Count -gt 0 -and ($args[0] -eq 'noon' -or $args[0] -eq 'day')) {
|
|
36
|
+
$mode = $args[0]
|
|
37
|
+
$rest = if ($args.Count -gt 1) { @($args[1..($args.Count - 1)]) } else { @() }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
$today = (Get-Date).ToString('yyyy-MM-dd')
|
|
41
|
+
$yesterday = (Get-Date).AddDays(-1).ToString('yyyy-MM-dd')
|
|
42
|
+
$targetDay = if ($mode -eq 'noon') { $today } else { $yesterday }
|
|
43
|
+
|
|
44
|
+
$stamp = Get-Date -Format 'yyyy-MM-dd'
|
|
45
|
+
$suffix = if ($mode -eq 'noon') { '-noon' } else { '' }
|
|
46
|
+
$out = Join-Path $logDir ('daily-{0}{1}.log' -f $stamp, $suffix)
|
|
47
|
+
$err = Join-Path $logDir ('daily-{0}{1}.err.log' -f $stamp, $suffix)
|
|
48
|
+
|
|
49
|
+
$env:PYTHONIOENCODING = 'utf-8'
|
|
50
|
+
$env:PYTHONUTF8 = '1'
|
|
51
|
+
|
|
52
|
+
$python = (Get-Command python.exe).Source
|
|
53
|
+
$script = Join-Path $PSScriptRoot 'pipeline.py'
|
|
54
|
+
$modeArgs = if ($mode -eq 'noon') { @('--date', $today) } else { @('--yesterday') }
|
|
55
|
+
$allArgs = @($script) + $modeArgs + @('--prefetch-bodies') + $rest
|
|
56
|
+
|
|
57
|
+
# Idempotent "make sure the target day is as complete as it can be right now", not
|
|
58
|
+
# "run at 08:20".
|
|
59
|
+
#
|
|
60
|
+
# Why: this machine slept from 2026-10-17 02:09 to 2026-10-18 15:26 and BOTH 08:20
|
|
61
|
+
# triggers were counted as missed while StartWhenAvailable=On did not start the task
|
|
62
|
+
# on wake (NumberOfMissedRuns=2, no run). A schedule that only fires if the machine
|
|
63
|
+
# happens to be awake at one instant is not a schedule. With the guard below the task
|
|
64
|
+
# can be triggered cheaply and often (schedule + at logon): if the target is already
|
|
65
|
+
# done this exits in milliseconds, otherwise it produces/extends the day.
|
|
66
|
+
#
|
|
67
|
+
# The day-mode guard deliberately does NOT trust index.html alone: the noon pass also
|
|
68
|
+
# writes one, so "index.html exists" would make the 00:05 pass skip the afternoon and
|
|
69
|
+
# leave the day half-done behind a complete-looking page. `.articles.json`'s
|
|
70
|
+
# generated_at tells us when the last pass actually ran; anything during that day
|
|
71
|
+
# (before 23:00) is a mid-day artifact and must be extended.
|
|
72
|
+
#
|
|
73
|
+
# Note the day pass runs at 00:05 **the next day**, so a finished day is one whose
|
|
74
|
+
# generated_at is later than the day itself - not one stamped at 23:xx. Testing
|
|
75
|
+
# `hour -ge 23` alone would have made every 00:05 run redo the whole day (observed
|
|
76
|
+
# while verifying this guard: generated_at 00:20 was judged incomplete).
|
|
77
|
+
function Test-DayComplete([string]$dir, [string]$targetDay) {
|
|
78
|
+
$idx = Join-Path $dir 'index.html'
|
|
79
|
+
if (-not (Test-Path $idx)) { return $false }
|
|
80
|
+
$art = Join-Path $dir '.articles.json'
|
|
81
|
+
if (-not (Test-Path $art)) { return $true } # old artifact without json: trust it
|
|
82
|
+
try {
|
|
83
|
+
$j = Get-Content $art -Raw -Encoding UTF8 | ConvertFrom-Json
|
|
84
|
+
$gen = [datetime]::ParseExact([string]$j.generated_at, 'yyyy-MM-dd HH:mm:ss', $null)
|
|
85
|
+
$genDay = $gen.ToString('yyyy-MM-dd')
|
|
86
|
+
# finished = generated on a later day (the usual 00:05 pass), or late the same night
|
|
87
|
+
return (($genDay -gt $targetDay) -or (($genDay -eq $targetDay) -and ($gen.Hour -ge 23)))
|
|
88
|
+
} catch { return $true }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
# Skipped when arguments are passed, so `daily_task.ps1 --help` still works as a
|
|
92
|
+
# smoke test with no side effects.
|
|
93
|
+
if ($rest.Count -eq 0) {
|
|
94
|
+
$dayDir = Join-Path $root ('output\biz-daily\' + $targetDay)
|
|
95
|
+
$done = if ($mode -eq 'noon') { Test-Path (Join-Path $dayDir '.noon_done') } else { Test-DayComplete $dayDir $targetDay }
|
|
96
|
+
if ($done) {
|
|
97
|
+
$note = '===== {0} [{1}] {2} already complete - skip =====' -f `
|
|
98
|
+
(Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $mode, $targetDay
|
|
99
|
+
Add-Content -Path (Join-Path $logDir 'daily-task.log') -Value $note -Encoding UTF8
|
|
100
|
+
exit 0
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
# Start-Process OVERWRITES its redirect targets, so a same-day rerun would erase the log
|
|
105
|
+
# of the run that just failed - exactly the one worth keeping. Rotate any existing pair to
|
|
106
|
+
# daily-<date>.run<N>[.err].log first, so every attempt stays on disk. N is the first free
|
|
107
|
+
# index, which also works when several reruns happen in one day.
|
|
108
|
+
if (Test-Path $out) {
|
|
109
|
+
$n = 1
|
|
110
|
+
while (Test-Path (Join-Path $logDir ('daily-{0}{1}.run{2}.log' -f $stamp, $suffix, $n))) { $n++ }
|
|
111
|
+
Move-Item -Path $out -Destination (Join-Path $logDir ('daily-{0}{1}.run{2}.log' -f $stamp, $suffix, $n))
|
|
112
|
+
if (Test-Path $err) {
|
|
113
|
+
Move-Item -Path $err -Destination (Join-Path $logDir ('daily-{0}{1}.run{2}.err.log' -f $stamp, $suffix, $n))
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
# Start-Process writes the raw bytes through: no PowerShell error-record wrapping, and
|
|
118
|
+
# no shell quoting to get wrong.
|
|
119
|
+
$proc = Start-Process -FilePath $python -ArgumentList $allArgs -NoNewWindow -Wait -PassThru `
|
|
120
|
+
-RedirectStandardOutput $out -RedirectStandardError $err
|
|
121
|
+
|
|
122
|
+
# Fallback for the HTML stage.
|
|
123
|
+
#
|
|
124
|
+
# `pipeline.py` fetches, summarises, writes the day, then renders `index.html`. That last step
|
|
125
|
+
# has hung twice now (2026-10-18: 11 hours with no output; 2026-10-23: the run died after the
|
|
126
|
+
# README and never produced the page). Losing the page also loses the mail, because mailing
|
|
127
|
+
# happens only on success - so a single slow step silently costs the whole morning report.
|
|
128
|
+
#
|
|
129
|
+
# The fetch and summaries are already on disk at that point (`.articles.json`), so rendering is
|
|
130
|
+
# retried on its own. `$proc` is replaced by the retry's exit code, which is what lets the mail
|
|
131
|
+
# and index steps below run on a day where only the renderer failed.
|
|
132
|
+
if ($proc.ExitCode -ne 0) {
|
|
133
|
+
$dayDir = Join-Path $root ('output\biz-daily\' + $targetDay)
|
|
134
|
+
$art = Join-Path $dayDir '.articles.json'
|
|
135
|
+
$idx = Join-Path $dayDir 'index.html'
|
|
136
|
+
$htmlScript = Join-Path $PSScriptRoot 'generate_html.py'
|
|
137
|
+
# Only retry when the data is there and the page is missing or older than the data:
|
|
138
|
+
# a page newer than `.articles.json` means rendering already happened this run.
|
|
139
|
+
$needsRender = (Test-Path $art) -and (Test-Path $htmlScript)
|
|
140
|
+
if ($needsRender -and (Test-Path $idx)) {
|
|
141
|
+
$needsRender = (Get-Item $art).LastWriteTime -gt (Get-Item $idx).LastWriteTime
|
|
142
|
+
}
|
|
143
|
+
if ($needsRender) {
|
|
144
|
+
Add-Content -Path (Join-Path $logDir 'daily-task.log') -Value `
|
|
145
|
+
('===== {0} [{1}] {2} pipeline exit {3}, retrying HTML only =====' -f `
|
|
146
|
+
(Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $mode, $targetDay, $proc.ExitCode) -Encoding UTF8
|
|
147
|
+
$retryProc = Start-Process -FilePath $python `
|
|
148
|
+
-ArgumentList @($htmlScript, '--date', $targetDay) `
|
|
149
|
+
-NoNewWindow -Wait -PassThru `
|
|
150
|
+
-RedirectStandardOutput (Join-Path $logDir ('html-retry-{0}{1}.log' -f $stamp, $suffix)) `
|
|
151
|
+
-RedirectStandardError (Join-Path $logDir ('html-retry-{0}{1}.err.log' -f $stamp, $suffix))
|
|
152
|
+
if ($retryProc.ExitCode -eq 0) {
|
|
153
|
+
$proc = $retryProc
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if ($proc.ExitCode -eq 0 -and $rest.Count -eq 0) {
|
|
159
|
+
# Marker for the noon pass only; the day pass is judged by generated_at above.
|
|
160
|
+
if ($mode -eq 'noon') {
|
|
161
|
+
$dayDir = Join-Path $root ('output\biz-daily\' + $targetDay)
|
|
162
|
+
if (Test-Path $dayDir) {
|
|
163
|
+
Set-Content -Path (Join-Path $dayDir '.noon_done') -Value (Get-Date -Format 'yyyy-MM-dd HH:mm:ss') -Encoding UTF8
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
# Retrieval indexes lag behind unless something refreshes them: on 2026-10-22 the
|
|
168
|
+
# semantic index was still at 10-15 (7 days stale) and the phrase index at 10-17, so
|
|
169
|
+
# `search` simply could not find anything newer. Both run here, after the day is on
|
|
170
|
+
# disk, in their own logs -- a failure is recorded and never fails the day.
|
|
171
|
+
$searchScript = Join-Path $PSScriptRoot 'semantic_search.py'
|
|
172
|
+
if ($mode -eq 'day' -and (Test-Path $searchScript)) {
|
|
173
|
+
$idxOut = Join-Path $logDir ('index-{0}.log' -f $stamp)
|
|
174
|
+
$idxErr = Join-Path $logDir ('index-{0}.err.log' -f $stamp)
|
|
175
|
+
# `update` only embeds what is not in the index yet (4,061 new records on the first
|
|
176
|
+
# run after a week). `phrase-build` is local FTS, no API, so it rebuilds in full.
|
|
177
|
+
$idxProc = Start-Process -FilePath $python `
|
|
178
|
+
-ArgumentList @($searchScript, 'update', '--article-days', '30') `
|
|
179
|
+
-NoNewWindow -Wait -PassThru -RedirectStandardOutput $idxOut -RedirectStandardError $idxErr
|
|
180
|
+
$phraseProc = Start-Process -FilePath $python `
|
|
181
|
+
-ArgumentList @($searchScript, 'phrase-build') `
|
|
182
|
+
-NoNewWindow -Wait -PassThru `
|
|
183
|
+
-RedirectStandardOutput (Join-Path $logDir ('phrase-{0}.log' -f $stamp)) `
|
|
184
|
+
-RedirectStandardError (Join-Path $logDir ('phrase-{0}.err.log' -f $stamp))
|
|
185
|
+
if ($idxProc.ExitCode -ne 0 -or $phraseProc.ExitCode -ne 0) {
|
|
186
|
+
Add-Content -Path (Join-Path $logDir 'daily-task.log') -Value `
|
|
187
|
+
('===== {0} index update failed (update={1}, phrase={2}), see {3} =====' -f `
|
|
188
|
+
(Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $idxProc.ExitCode, $phraseProc.ExitCode, $idxErr) -Encoding UTF8
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
# **Both passes mail now.** The noon pass used to be silent, which read as "it never
|
|
193
|
+
# ran" - the only way to tell it had done anything was to open the log. The two emails
|
|
194
|
+
# are clearly different: the noon one is labelled 截至中午 and says the complete version
|
|
195
|
+
# follows next morning. Mail is still its own step with its own log pair, so a mail
|
|
196
|
+
# failure never fails the day nor blocks the noon marker.
|
|
197
|
+
$mailScript = Join-Path $PSScriptRoot 'daily_mail.py'
|
|
198
|
+
$mailOut = Join-Path $logDir ('daily-mail-{0}{1}.log' -f $stamp, $suffix)
|
|
199
|
+
$mailErr = Join-Path $logDir ('daily-mail-{0}{1}.err.log' -f $stamp, $suffix)
|
|
200
|
+
if (Test-Path $mailScript) {
|
|
201
|
+
$mailArgs = @($mailScript, '--date', $targetDay)
|
|
202
|
+
if ($mode -eq 'noon') { $mailArgs += @('--label', '截至中午') }
|
|
203
|
+
$mailProc = Start-Process -FilePath $python -ArgumentList $mailArgs `
|
|
204
|
+
-NoNewWindow -Wait -PassThru -RedirectStandardOutput $mailOut -RedirectStandardError $mailErr
|
|
205
|
+
if ($mailProc.ExitCode -ne 0) {
|
|
206
|
+
Add-Content -Path (Join-Path $logDir 'daily-task.log') -Value `
|
|
207
|
+
('===== {0} [{1}] {2} mail failed (exit {3}), see {4} =====' -f `
|
|
208
|
+
(Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $mode, $targetDay, $mailProc.ExitCode, $mailErr) -Encoding UTF8
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
exit $proc.ExitCode
|