feat(due): 事件偵測與到期判定,七個事件名接上狀態來源

待辦簿存得住 trigger 與 recur,但沒有東西判「現在到期了沒有」。存放那一支的檔頭明寫它不判定、算的邏輯在別的地方,這一輪就是補那個別的地方。

另開一支而不併進存放那一支。那一支的檔頭把「我不判定」寫成合約,而合約是呼叫端唯一的依據;併進去就要推翻那整段話,而讀過舊版的人會以為那一支不判、自己再判一次,於是漂移從註解裡長出來。界線做成單向:判定這一支只讀待辦檔與印值,要改狀態一律回頭叫存放那一支的完成與失敗。

事件偵測靠比對狀態快照,不改任何產生者。每一輪讀那幾支產生者留下的狀態,跟上一輪存下的快照比,有差別就算事件發生。六個事件名各自接上本機狀態來源:工作包鎖檔轉態、階段檔換值、錯誤紀錄行數變多、工作階段開始與結束、日誌暫存區清空。

這個選擇有一個明確的代價,寫進檔頭:兩輪之間發生又消失的事件會漏掉。往後誰假設「事件不會漏」就會出錯,而那種錯是無聲的。不為了補這個洞去改那幾支產生者——要它們各自在事件發生那一刻多寫一筆,任何一支忘了寫就讓對應的事件從此不再發生,而那同樣無聲,還散在五個 domain 裡查不出是誰漏的。快照比對至少壞掉時看得出來。

第一次跑一律只建快照、不發事件,否則機器上所有既有狀態都會被當成剛發生,讓每一筆事件型待辦一次全到期。同一條理由逐個來源再套一次:某個來源上一輪不存在、這一輪才出現,那個來源這一輪也只建快照。所以快照要留一列記每個來源上一輪是看得到還是不存在——少了它就分不出「上一輪沒有這個來源」與「上一輪這個來源是空的」,而只有後者的差別才算事件。來源目錄從有變成不見時也不發事件,只記警示:整個目錄被砍掉不是每一支工作包都合併了。

四種組合一個都不壓。時鐘型的排定點從那個時間往後數、一次加一個間隔,不從上次執行起算——每次拿上次執行加間隔,執行慢幾秒就往後挪幾秒,一天下來就偏掉。漏掉的排定點只補一次不逐個追補,否則機器關一天再開會連續跑幾十輪同一件事。事件型配間隔時,那個間隔照存放那一支的定義當「重新武裝的最短間隔」。

事件型的下次執行時間一律留空。事件型沒有預定時間,唯一算得出來的是重新武裝時刻,但那個值填進去會被狀態表與提醒讀成「那個時間會跑」,它其實只是「那個時間之前不會跑」,差別在畫面上看不出來。那個值改印成另一個欄位,要看的人看得到。

規格有兩條在同一個欄位上打架。同一件事不重複觸發的判準是「事件發生時間比上次執行晚」,但失敗也會寫上次執行,於是失敗過的事件型待辦再也等不到同一次事件,也就永遠不重試——而規格明確要求失敗照重試。改用連續失敗次數當第三個判準把兩者分開:狀態還是待辦而次數大於零,代表上一次執行失敗,一律算到期,不再比事件也不看排定點。完成會把次數歸零,所以成功那一次不會被誤判成要重試。

兩項刻意標成未接線並吵出來,不假裝算得出來。分析完成那一個事件的狀態來源在 wiki 上、要連網要金鑰,混進來之後金鑰一過期就讓事件偵測整項每輪失敗;更糟的是判不出來時那幾筆會靜靜地永遠不到期,跟填一個不存在的事件名一樣看不出壞在哪。cron 式子同理,五個欄位的萬用字元與步進算錯一格就是差一小時或差一天,而那種錯要等真的跑錯才看得出來。兩者的處置都是印一行未接線、列進要人接手那一節,明說「這一筆現在判不出到期,不是還沒發生」。

間隔格式規格從來沒定義過,這一輪定成正整數加單位,刻意不收裸數字——排程週期以分鐘算、心跳門檻以秒算,裸數字兩種都讀得通,讀錯就差六十倍。

巡檢那一路一併接上:快照要有人推進才活得起來。刻意不把它算進既有五項的成敗,避免動到摘要表的欄名與本輪項目那一列的文字,舊頁那些列的欄名不會跟著改,同一張表就會有兩種寫法。失敗時改成記警示與待人處理。

順手修掉一個同類的坑:用冒號當空指令去清空檔案時,重導向失敗會讓整個殼直接結束,後面的分流一次都跑不到。這一支一律改用 true 並把理由寫進註解。
This commit is contained in:
2026-09-03 16:37:44 +08:00
parent e7edfcbec0
commit 737fcf13fd
3 changed files with 1254 additions and 8 deletions
File diff suppressed because one or more lines are too long
Executable
+1161
View File
File diff suppressed because it is too large Load Diff
+89 -4
View File
@@ -61,6 +61,10 @@
# D-11 是唯一會動到別人狀態的一項:drain 會把事件流的位移往前推。理由與配套見下面
# 「執行狀態事件為什麼由這支排空」。
#
# 待辦簿那一節不在這五項裡面:它由 tools/due.sh 的 scan 組好,這支只叫它一次再把那一塊接進頁面。
# 它也會動自己的狀態——推進待辦簿的事件快照,性質同 drain:同一輪叫第二次就比不出東西了。
# 那一項的成敗不算進上面五項的成敗,理由寫在 due_scan() 那一段。
#
# --- version-guard.sh report 的既有缺陷照實記 ---
#
# 這支腳本在部分機器上對每一個 domain 都回「查詢失敗」,recommend 跟著回 unverifiable。
@@ -143,7 +147,11 @@
# failed_sources= 讀不到的來源路徑,以「、」分隔;全部讀得到就是「無」
# warn_sources= 本輪的警示來源,以「、」分隔;沒有警示就是「無」。各項全過卻判成警示
# 時,原因只寫在這裡
# tasks_total= tasks_failing= 待辦簿筆數與連續失敗筆數,只供目錄頁那一個區塊用
# tasks_total= tasks_failing= 待辦簿筆數與連續失敗筆數,供目錄頁那一個區塊與摘要用
# tasks_due= 本輪到期的筆數;到期判定那一支失敗時為空
# events_new= 本輪偵測到的新事件種類數;到期判定那一支失敗時為空
# due_status= due_rc= 到期判定那一支的結果與結束碼
# due_file= 到期判定那一支組好的「待辦簿到期與逾期」那一節,已經接進 latest_file
# pending= 本輪待人處理的筆數
# events_total= 本輪排空到的事件筆數
# events_bad= 其中 status 不是 ok 的筆數
@@ -194,6 +202,12 @@ WARN_SOURCES=''
OK_COUNT=0
FAIL_COUNT=0
WARN=0
DUE_STATUS=fail
DUE_RC=0
DUE_MD=''
DUE_NOTE=''
DUE_TASKS=''
DUE_EVENTS=''
# 這支腳本是不是從 $JSC_HOME/current 那一組路徑被叫起來的。不是就大聲警告,但照跑。
# 只警告、不中止是刻意的取捨:從工作樹直接跑腳本是開發時的正當用法,中止會把那條路擋掉;
@@ -872,6 +886,65 @@ count_tasks() {
return 0
}
# --- 待辦簿的事件偵測與到期判定 ---
#
# 算到期的邏輯不在這一支,也不在 tools/tasks.sh,而在 tools/due.sh,理由寫在那一支的檔頭:
# 存放與判定各一支,兩邊各算一次就會漂移。這裡只負責在一輪之內叫它一次。
# 一輪只叫一次,而且只叫 scan:scan 會推進狀態快照,同一輪叫第二次就比不出任何事件了。
# 要看「下一輪會判出什麼」而不想動快照,叫那一支的 events,那個子命令一律唯讀。
#
# 這一項刻意不算進上面那五項的成敗。它補的是既有那一節的內容,不是新增一個獨立來源;算進去
# 就要改動摘要表那一欄與「本輪項目」那一列的文字,而舊頁那些列的欄名不會跟著改,同一張表就
# 會有兩種寫法。失敗時記一筆警示、列一筆待人處理,那一節照實寫「這一輪判不出到期」。
due_scan() {
DUE_STATUS=fail; DUE_RC=0; DUE_MD=''; DUE_NOTE=''
DUE_TASKS=''; DUE_EVENTS=''
_due="$SCRIPT_DIR/due.sh"
if [ ! -f "$_due" ]; then
DUE_NOTE="找不到($_due)"
add_failed_source "$_due"
add_warn '到期判定那一支找不到'
add_pending '待辦簿的到期判定讀不到,jsc-assist 沒裝齊' '待辦簿到期與逾期' '/jsc-cli:deploy'
return 0
fi
_rc=0
JSC_HOME="$JSC_HOME" sh "$_due" scan --out "$RD/due" >"$RD/due.out" 2>"$RD/due.err" </dev/null || _rc=$?
DUE_RC="$_rc"
DUE_MD=$(sed -n 's/^due_file=//p' "$RD/due.out" 2>/dev/null | head -n1)
DUE_TASKS=$(sed -n 's/^tasks_due=//p' "$RD/due.out" 2>/dev/null | head -n1)
DUE_EVENTS=$(sed -n 's/^events_new=//p' "$RD/due.out" 2>/dev/null | head -n1)
case "$_rc" in
0) DUE_STATUS=ok ;;
4)
# 待辦簿零筆或目錄不存在。助理還沒收過待辦就是這個狀態,不是失敗。
DUE_STATUS=empty ;;
1)
DUE_STATUS=ok
DUE_NOTE='有狀態來源讀不到'
add_warn '到期判定有狀態來源讀不到'
add_pending '待辦簿事件偵測有狀態來源讀不到,那個來源本輪不發事件' '待辦簿到期與逾期' '/jsc-cli:setup' ;;
2)
DUE_STATUS=ok
DUE_NOTE='有待辦的欄位值判不了'
add_warn '有待辦的欄位值判不了'
add_pending '有待辦的 trigger 或 recur 判不了,那幾筆永遠不會到期' '待辦簿到期與逾期' '/jsc-assist:assistant status' ;;
3)
# 快照換不上去是這幾碼裡最要吵的一個:事件算出來了,但下一輪會拿同一份舊快照再比一次,
# 同一批事件於是被判第二次,同一件事就做了兩遍。
DUE_STATUS=ok
DUE_NOTE='狀態快照換不上去,下一輪會重判同一批事件'
add_warn '事件快照換不上去'
add_pending '事件快照換不上去,同一批事件下一輪會被判第二次' '待辦簿到期與逾期' '/jsc-cli:setup' ;;
*)
DUE_NOTE="回結束碼 $_rc:$(tr '\n' ' ' <"$RD/due.err" 2>/dev/null)"
DUE_MD=''
add_failed_source "$_due"
add_warn '到期判定回非預期結束碼'
add_pending "待辦簿的到期判定回 $_rc,本輪判不出到期" '待辦簿到期與逾期' '/jsc-assist:assistant status' ;;
esac
return 0
}
# --- 組出監控頁那三塊 ---
tally() { # $1=項目狀態
@@ -915,9 +988,15 @@ compose() {
printf '**這一輪不做這一項。** D-02 hook 錯誤巡檢還沒實作,這一節沒有資料不代表沒有 hook 錯誤。要現在查就跑 `/jsc-hooks:hooks-install` 的錯誤掃描,或直接跑 `jsc-hooks` 的 `tools/scan-hook-errors.sh`。\n\n'
cat "$RD/d04.md"; printf '\n'
cat "$RD/d07.md"; printf '\n'
printf '### 待辦簿到期與逾期\n\n'
printf '**這一輪只數筆數,不逐筆判到期。** 本輪待辦簿共 %s 筆,其中 %s 筆 `fail_count` 大於 0。逐筆的到期與逾期判定還沒實作,要看逐筆內容就跑 `/jsc-assist:assistant status`。\n\n' \
"$TASKS_TOTAL" "$TASKS_FAILING"
if [ -n "$DUE_MD" ] && [ -s "$DUE_MD" ]; then
cat "$DUE_MD"
printf '\n本輪待辦簿共 %s 筆,其中 %s 筆 `fail_count` 大於 0。逾期(`due` 欄已經過了)還沒實作,上面的判定只講到期,不講逾期。\n\n' \
"$TASKS_TOTAL" "$TASKS_FAILING"
else
printf '### 待辦簿到期與逾期\n\n'
printf '**這一輪判不出到期。** 到期判定那一支(`tools/due.sh`)%s。本輪待辦簿共 %s 筆,其中 %s 筆 `fail_count` 大於 0。這一節沒有判定結果**不代表沒有任何一筆到期**,要逐筆看就跑 `/jsc-assist:assistant status`。\n\n' \
"$(cell "${DUE_NOTE:-沒有回報原因}")" "$TASKS_TOTAL" "$TASKS_FAILING"
fi
printf '### 待人處理\n\n'
printf '助理只提醒,不代為執行。這一節列的是本輪要人接手的項目。\n\n'
printf '| 項目 | 來源子節 | 建議入口 |\n'
@@ -1040,6 +1119,7 @@ case "$CMD" in
d07
d11
count_tasks
due_scan
tally "$D01_STATUS"; tally "$D04_STATUS"; tally "$D07_STATUS"; tally "$D09_STATUS"
tally "$D11_STATUS"
@@ -1069,6 +1149,11 @@ case "$CMD" in
printf 'warn_sources=%s\n' "${WARN_SOURCES:-無}"
printf 'tasks_total=%s\n' "$TASKS_TOTAL"
printf 'tasks_failing=%s\n' "$TASKS_FAILING"
printf 'tasks_due=%s\n' "$DUE_TASKS"
printf 'events_new=%s\n' "$DUE_EVENTS"
printf 'due_status=%s\n' "$DUE_STATUS"
printf 'due_rc=%s\n' "$DUE_RC"
printf 'due_file=%s\n' "$DUE_MD"
printf 'pending=%s\n' "$PEND_COUNT"
printf 'events_total=%s\n' "$EV_TOTAL"
printf 'events_bad=%s\n' "$EV_BAD"