feat(狀態回報): 收尾寫一筆 skill-end 事件

現行紀錄只記「被叫用」,沒有成敗也沒有結束碼。跑完整輪的技能與開場就
中止的技能,在紀錄裡長得一模一樣。

start 由技能用量 hook 順手發,不必改技能文件。end 只能由技能自己在收尾
步驟寫——hook 接在技能工具呼叫上,而實際工作發生在之後的模型輪次,它在
原理上看不到成敗。有 start 沒有配對的 end,就是那一輪中止了。

status 五選一,每支技能各自寫明什麼情況選哪一個。找不到回報腳本就安靜
跳過,回報失敗一律不改變技能自己的結論。
This commit is contained in:
2026-09-02 16:01:13 +08:00
parent 94566026c7
commit 3dac1475df
7 changed files with 390 additions and 39 deletions
+277 -17
View File
@@ -8,18 +8,18 @@
#
# collect 帶了 --out,finish 與 abort 就要帶同一個目錄,不然換不到本輪的用量快照。
#
# collect 讀四項來源、組出監控頁那三塊、把鎖拿在手上。
# collect 讀各項來源、組出監控頁那三塊、把鎖拿在手上。
# finish 在監控頁寫成功之後才呼叫:寫心跳、換上用量快照、放掉鎖。
# abort 在監控頁沒寫成時呼叫:只放掉鎖,不寫心跳。
#
# 結束碼(三個子命令共用一張表,同一碼在不同子命令的成因寫在同一列):
# 0 collect:四項全部讀到底(含「來源在、沒有資料」);finish:心跳寫好、快照換上、
# 0 collect:各項全部讀到底(含「來源在、沒有資料」);finish:心跳寫好、快照換上、
# 鎖放掉;abort:鎖放掉,本來就沒鎖也算
# 1 collect:部分成功——至少一項失敗,也至少一項有結果。**結果照樣印得出來,呼叫端
# 照樣要把這一輪寫上監控頁**,只是本輪判定要標成警示
# 2 finish:找不到 jsc-hooks 的 hooks/heartbeat.sh,心跳沒有東西可寫。collect 不會回這
# 一碼——心跳讀不到只是 D-09 這一項失敗,另外三項照跑
# 3 collect:四項全部失敗,一項資料都沒有。這一輪還是要寫上監控頁,本輪判定標成異常
# 一碼——心跳讀不到只是 D-09 這一項失敗,其餘各項照跑
# 3 collect:各項全部失敗,一項資料都沒有。這一輪還是要寫上監控頁,本輪判定標成異常
# 4 上一輪還在跑,本輪讓開(collect),或鎖已經不在自己手上(finish、abort)。這不是
# 失敗,是刻意讓開:不寫心跳、不寫監控頁,下一輪再來
# 5 檔案系統失敗:鎖建不起來或放不掉、暫存檔寫不進去、快照換不上,或 heartbeat.sh write
@@ -34,7 +34,7 @@
#
# --- 心跳寫不寫,只看結果有沒有記下來 ---
#
# 四項的成敗不決定心跳。四項全失敗但監控頁寫成了,那一輪還是跑完了,證據也留下來了,
# 各項的成敗不決定心跳。各項全失敗但監控頁寫成了,那一輪還是跑完了,證據也留下來了,
# 心跳照寫,頁上判定是異常,看頁的人自己判斷。反過來,監控頁沒寫成就是這一輪沒有結果,
# 心跳一定不寫:讓它自己過期,就是「巡檢在空轉」的唯一訊號。
# 所以寫心跳一定是獨立的 finish,時序上排在監控頁寫成之後,不與 collect 綁在一起。
@@ -49,14 +49,17 @@
# 拿 --round 比對出鎖不是自己的,回 4 且不寫心跳。
# 搶回來這件事會記在監控頁上(lock_broken=1),不會安靜發生。
#
# --- 這四項都是純讀取 ---
# --- 這幾項都是純讀取 ---
#
# D-01 技能與呼叫鏈使用統計 jsc-log 的 tools/usage-stats.sh
# D-04 版本落差與重啟閘門 jsc-hooks 的 version-guard.sh report、restart-gate.sh report
# D-07 SDLC 階段鎖與工作包鎖 $JSC_HOME/sessions/*.stage、$JSC_HOME/wp/*.pr
# D-09 心跳與閘門狀態自述 jsc-hooks 的 heartbeat.sh report
# 四項各自獨立:一項的來源不見了、或回非 0,只讓那一項標成失敗,其餘三項照跑、照記。
# 四項都不呼叫別的技能、不寫程式碼存取庫、不做決策。
# D-11 執行狀態事件 jsc-hooks 的 tools/report-status.sh drain
# 各項各自獨立:一項的來源不見了、或回非 0,只讓那一項標成失敗,其餘各項照跑、照記。
# 各項都不呼叫別的技能、不寫程式碼存取庫、不做決策。
# D-11 是唯一會動到別人狀態的一項:drain 會把事件流的位移往前推。理由與配套見下面
# 「執行狀態事件為什麼由這支排空」。
#
# --- version-guard.sh report 的既有缺陷照實記 ---
#
@@ -100,6 +103,30 @@
# 列,舊列從此不再更新。這一頁每 15 分鐘寫一次,重複列累積得很快。裸 HASH 只由
# {主機名}/{登入帳號} 決定,上面三件事都動不到它。
#
# --- 執行狀態事件為什麼由這支排空 ---
#
# 事件流記的是每一支技能與每一支 hook 的執行結果。技能的 start 由 hook 免費記下,end 只能由
# 技能自己在收尾時寫,所以「有 start 沒有配對的 end」就是那一輪中止了——那是這整套機制唯一
# 分得出中止的訊號,也是這一項最重要的產出。
#
# 排空與彙整放在這支,不放在技能本文,有三個理由:
# 1. drain 是消耗性讀取:它一讀完就把位移往前推,同一批事件不會再出現第二次。讀回來的內容
# 只存在對話裡的話,模型少抄一行就是那一批事件永遠消失。寫成檔案才留得住。
# 2. 一輪的事件動輒上百行 JSON,逐行判 status 與配對 start/end 交給模型做,既慢又會出錯。
# 3. rotate 一定要緊接在 drain 後面跑。中間隔得越久,那段時間新寫進來的事件被搬進備份檔
# 而從此不會被排空的機率越高。兩件事綁在同一支腳本的同一次執行,那個空窗才最小。
#
# 配對以 {session}+{name} 為鍵,不只看 name:五支 CLI 併發時同一支技能會有好幾個工作階段同時
# 在跑,只看 name 會讓 A 工作階段的 end 去配掉 B 工作階段的 start,中止就被蓋掉了。
#
# 沒配對到的 start 會留在 $JSC_HOME/assistant/events-open.tsv,跨輪繼續配對。不留的話,一支
# 跑超過一個巡檢週期的技能每一輪都會被報成中止——巡檢週期預設兩分鐘,那種誤報會多到沒人看。
# 開著超過心跳門檻才算「疑似中止」,門檻以內的算「進行中」,兩種分開列。超過一天還沒配對到的
# 就從檔案裡丟掉,那個檔案才不會無止境長大。
#
# 這一項失敗(找不到腳本、drain 回非預期結束碼、rotate 失敗)一律只讓這一項標成失敗或記一筆
# 警示,不中止整輪:回報鏈自己壞掉,不可以把被回報的那一輪也拖下去。
#
# --- collect 的輸出 ---
#
# stdout 是 key=value,一行一個鍵,供呼叫端逐行取值。監控頁要用的 markdown 不印在
@@ -113,10 +140,17 @@
# item= 一項一行,欄位 status(ok、empty、fail)、rc、note
# verdict= 正常、警示、異常
# failed_sources= 讀不到的來源路徑,以「、」分隔;全部讀得到就是「無」
# warn_sources= 本輪的警示來源,以「、」分隔;沒有警示就是「無」。四項全過卻判成警示
# warn_sources= 本輪的警示來源,以「、」分隔;沒有警示就是「無」。各項全過卻判成警示
# 時,原因只寫在這裡
# tasks_total= tasks_failing= 待辦簿筆數與連續失敗筆數,只供目錄頁那一列用
# pending= 本輪待人處理的筆數
# events_total= 本輪排空到的事件筆數
# events_bad= 其中 status 不是 ok 的筆數
# events_unpaired= 有 start 沒有配對 end、而且已經開超過心跳門檻的筆數(疑似中止)
# events_running= 有 start 沒有配對 end,但還在門檻以內的筆數(還在跑)
# events_rotated= rotated、not-needed、failed、skipped 四選一
# events_file= 本輪排空到的原始事件,一行一筆 JSON。drain 是消耗性讀取,這個檔案是
# 那一批事件在被彙整之外唯一留下的完整原文
# latest_file= 「最新一輪」那一塊,整塊換掉舊頁同名那一塊
# summary_file= 「近 24 輪摘要」那一塊,表格裡先放本輪這一列,舊頁的資料列接在下面
# summary_row_file= 只有本輪那一列,方便直接插到既有表格最上面
@@ -133,6 +167,7 @@
# JSC_ASSIST_RESTART_GATE_SH restart-gate.sh 路徑覆寫
# JSC_ASSIST_USAGE_STATS_SH usage-stats.sh 路徑覆寫
# JSC_ASSIST_HASH_ID hash-id 路徑覆寫
# JSC_ASSIST_REPORT_STATUS_SH report-status.sh 路徑覆寫
# JSC_ASSIST_PATROL_LOCK_TTL 鎖的逾時秒數;未設定時取心跳門檻,取不到就用 300
set -u
@@ -141,6 +176,9 @@ STATE_DIR="$JSC_HOME/assistant"
CURRENT="$JSC_HOME/current"
LOCK="$STATE_DIR/patrol.lock"
PREV_SNAP="$STATE_DIR/usage-prev.tsv"
# 還沒配對到 end 的 start,跨輪留在這裡。放 $JSC_HOME/assistant 而不放 $RD:$RD 每一輪重寫,
# 放那裡就等於不跨輪,跑超過一個週期的技能每輪都會被報成中止。
EVENTS_OPEN="$STATE_DIR/events-open.tsv"
RD="$STATE_DIR/patrol"
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" 2>/dev/null && pwd)
@@ -173,7 +211,7 @@ warn_if_not_current() {
warn_if_not_current
# 記一個警示來源。每一處把 WARN 設成 1 的地方都經過這裡,摘要表那一欄才看得出警示哪來——
# 四項全過卻判成警示,光看成敗欄是查不出原因的。
# 各項全過卻判成警示,光看成敗欄是查不出原因的。
add_warn() { # $1=一句話講完的理由
WARN=1
if [ -z "$WARN_SOURCES" ]; then WARN_SOURCES="$1"; else WARN_SOURCES="$WARN_SOURCES、$1"; fi
@@ -592,7 +630,7 @@ d09() {
printf '| pid | %s。只給要找行程的人參考,不參與判定 |\n' "$(cell "${_pid:--}")"
printf '| 心跳檔 | `%s` |\n' "$(cell "${_file:--}")"
printf '\n心跳的判準只看 `ts` 距現在有沒有超過門檻,不看 pid 存活:五支 CLI 與容器裡的行程互相看不到彼此的 pid。閘門的判定留在 hook,助理只維持心跳,不參與判定。\n'
printf '\n心跳新鮮代表上一輪巡檢跑完了,而且結果記上監控頁了。它不代表那一輪四項都成功——四項的成敗看這一塊上面的「本輪判定」。\n'
printf '\n心跳新鮮代表上一輪巡檢跑完了,而且結果記上監控頁了。它不代表那一輪各項都成功——各項的成敗看這一塊上面的「本輪判定」。\n'
} >>"$RD/d09.md"
case "$_st" in
@@ -603,6 +641,216 @@ d09() {
return 0
}
# --- D-11 執行狀態事件 ---
# 一輪最多列幾筆明細。不設上限的話,一次壞掉的 hook 每次提示寫一筆,一輪就能把整頁灌爆,
# 而灌爆的頁沒有人讀得完,等於這一節白寫。列不下的用一句話講清楚還有幾筆。
EV_MAX_ROWS=50
# 把事件流的一行 JSON 攤成定位字元分隔的欄位。不引 JSON 解析器:巡檢跑在 cron 上,能倚賴的
# 只有系統本來就有的工具,多一個相依就是多一種在某台機器上跑不起來的方式。
# 取值以「鍵名加冒號加引號」定位,取到下一個引號為止。detail 裡若有被跳脫的引號會在那裡被切斷,
# 那只影響顯示的長度,不影響筆數與配對,所以不為它多寫一套解析。
events_to_tsv() { # $1=原始事件檔 $2=輸出檔
awk '
function jstr(s, k, r) {
if (match(s, "\"" k "\":\"")) {
r = substr(s, RSTART + length(k) + 4)
if (match(r, "\"")) return substr(r, 1, RSTART - 1)
}
return ""
}
function jnum(s, k, r) {
if (match(s, "\"" k "\":[0-9]+")) { r = substr(s, RSTART, RLENGTH); sub(/^.*:/, "", r); return r }
return ""
}
{
gsub(/\t/, " ")
printf "%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n", \
jstr($0, "ts"), jstr($0, "cli"), jstr($0, "session"), jstr($0, "kind"), jstr($0, "name"), \
jstr($0, "phase"), jstr($0, "status"), jnum($0, "exit"), jstr($0, "detail")
}' "$1" >"$2" 2>/dev/null
}
d11() {
D11_STATUS=fail; D11_RC=0; D11_NOTE=''
EV_TOTAL=0; EV_BAD=0; EV_UNPAIRED=0; EV_RUNNING=0; EV_ROTATED=skipped
{
printf '### 執行狀態事件\n\n'
printf '資料出自 `%s`,由 `jsc-hooks` 的 `tools/report-status.sh drain` 排空,位移記在 `%s`。\n\n' \
"\$JSC_HOME/usage/events.jsonl" "\$JSC_HOME/usage/scan-state/events.offset"
printf '技能的 `start` 由 hook 記,`end` 只能由技能自己在收尾時寫。所以**有 `start` 沒有配對的 `end` 就是那一輪中止了**,配對以 `session` 加 `name` 為鍵。\n\n'
} >"$RD/d11.md"
if ! _rs=$(find_tool hooks tools/report-status.sh "${JSC_ASSIST_REPORT_STATUS_SH:-}"); then
D11_NOTE='找不到 jsc-hooks 的 tools/report-status.sh'
D11_RC=127
add_failed_source 'jsc-hooks/tools/report-status.sh'
printf '**這一項失敗**:%s。這一輪沒有執行狀態事件,不是「每一支都跑成功」。\n' "$D11_NOTE" >>"$RD/d11.md"
add_pending '執行狀態事件讀不到,jsc-hooks 沒裝或版本太舊' '執行狀態事件' '/jsc-cli:doctor'
return 0
fi
# 排空。結束碼 3 是「沒有新事件」,那是正常狀態,不是失敗——每一輪都排空,多數輪次本來
# 就沒有新事件。JSC_HOME 明寫成環境變數傳下去:這支腳本裡的 JSC_HOME 不見得是匯出的,
# 子行程自己算預設值時,兩邊指到同一個目錄才算數。
: >"$RD/events.raw"
_rc=0
JSC_HOME="$JSC_HOME" "$_rs" drain >"$RD/events.raw" 2>"$RD/d11.err" </dev/null || _rc=$?
if [ "$_rc" -ne 0 ] && [ "$_rc" -ne 3 ]; then
D11_RC="$_rc"
D11_NOTE="report-status.sh drain 回 $_rc"
add_failed_source "$_rs"
printf '**這一項失敗**:%s。訊息:%s\n' "$D11_NOTE" "$(cell "$(cat "$RD/d11.err" 2>/dev/null)")" >>"$RD/d11.md"
add_pending '事件流排空失敗,本輪沒有執行狀態證據' '執行狀態事件' '/jsc-hooks:repair'
return 0
fi
# 輪替緊接在排空後面跑,而且只在排空成功時跑。排空失敗時位移的狀態是未知的,這時候輪替
# 會把還沒排空的事件搬進備份檔,那一批從此不會再出現在任何一輪。
_rrc=0
_rout=$(JSC_HOME="$JSC_HOME" "$_rs" rotate 2>>"$RD/d11.err" </dev/null) || _rrc=$?
if [ "$_rrc" -ne 0 ]; then
EV_ROTATED=failed
add_warn '事件流輪替失敗'
add_pending "事件流輪替回 $_rrc,檔案會一直長大" '執行狀態事件' '/jsc-hooks:repair'
elif [ -n "$_rout" ]; then
EV_ROTATED=rotated
else
EV_ROTATED=not-needed
fi
events_to_tsv "$RD/events.raw" "$RD/events.tsv"
_now=$(date +%s)
_ttl="${HEARTBEAT_TTL:-}"
case "$_ttl" in ''|*[!0-9]*) _ttl=300 ;; esac
: >"$RD/events-bad.tsv"; : >"$RD/events-unpaired.tsv"; : >"$RD/events-open.next"
[ -f "$EVENTS_OPEN" ] || : >"$EVENTS_OPEN"
# 兩個輸入檔:先讀上一輪留下來還開著的 start,再讀本輪排空到的事件。用 FILENAME 分辨是
# 哪一個檔案,不用 NR==FNR:上一輪那個檔案是空的時候,NR==FNR 會把本輪第一筆事件誤當成
# 舊資料,而「上一輪沒有開著的 start」正是最常見的情況。
awk -F' ' -v prevf="$EVENTS_OPEN" -v now="$_now" -v ttl="$_ttl" -v maxage=86400 \
-v openf="$RD/events-open.next" -v badf="$RD/events-bad.tsv" -v unpf="$RD/events-unpaired.tsv" '
FILENAME == prevf { k = $1 SUBSEP $2; okind[k] = $3; oseen[k] = $4; ots[k] = $5; next }
{
total++
if ($7 != "ok") { bad++; printf "%s\t%s\t%s\t%s\t%s\t%s\n", $1, $4, $5, $7, $8, $9 >badf }
k = $3 SUBSEP $5
if ($6 == "start") {
okind[k] = $4
# 第一次看到才記時間:同一支技能在同一個工作階段重複開場時,年紀要從最早那一次算起。
if (!(k in oseen)) { oseen[k] = now; ots[k] = $1 }
} else if ($6 == "end") { delete okind[k]; delete oseen[k]; delete ots[k] }
}
END {
for (k in oseen) {
age = now - oseen[k]
# 開超過一天的丟掉。留著只會讓這個檔案無止境長大,而那麼久沒收尾的 start 早就報過了。
if (age > maxage) continue
split(k, p, SUBSEP)
printf "%s\t%s\t%s\t%s\t%s\n", p[1], p[2], okind[k], oseen[k], ots[k] >openf
if (age >= ttl) { unpaired++; printf "%s\t%s\t%s\t%s\t%s\n", p[2], okind[k], p[1], ots[k], age >unpf }
else running++
}
printf "total=%d bad=%d unpaired=%d running=%d\n", total, bad, unpaired, running
}' "$EVENTS_OPEN" "$RD/events.tsv" >"$RD/d11.counts" 2>>"$RD/d11.err"
_counts=$(cat "$RD/d11.counts" 2>/dev/null)
for _kv in $_counts; do
case "$_kv" in
total=*) EV_TOTAL="${_kv#total=}" ;;
bad=*) EV_BAD="${_kv#bad=}" ;;
unpaired=*) EV_UNPAIRED="${_kv#unpaired=}" ;;
running=*) EV_RUNNING="${_kv#running=}" ;;
esac
done
for _v in EV_TOTAL EV_BAD EV_UNPAIRED EV_RUNNING; do
eval "_x=\$$_v"
case "$_x" in ''|*[!0-9]*) eval "$_v=0" ;; esac
done
# 換上這一輪之後還開著的 start。換不上不算整項失敗:下一輪頂多重算一次年紀,不會漏報。
cp "$RD/events-open.next" "$EVENTS_OPEN" 2>/dev/null \
|| printf '**未配對清單存檔失敗**:`%s` 寫不進去,下一輪的年紀要重算。\n\n' "$EVENTS_OPEN" >>"$RD/d11.md"
{
printf '| 項目 | 內容 |\n'
printf '| --- | --- |\n'
printf '| 本輪事件數 | %s |\n' "$EV_TOTAL"
printf '| 非 ok 事件數 | %s |\n' "$EV_BAD"
printf '| 有 start 沒有 end(開超過 %s 秒,疑似中止) | %s |\n' "$_ttl" "$EV_UNPAIRED"
printf '| 有 start 沒有 end(未達門檻,還在跑) | %s |\n' "$EV_RUNNING"
printf '| 事件流輪替 | %s |\n' "$EV_ROTATED"
printf '\n#### 非 ok 事件明細\n\n'
} >>"$RD/d11.md"
if [ -s "$RD/events-bad.tsv" ]; then
{
printf '| 時間 | 類別 | 名稱 | status | 結束碼 | detail |\n'
printf '| --- | --- | --- | --- | ---: | --- |\n'
} >>"$RD/d11.md"
_n=0
while IFS=' ' read -r _ets _ekind _ename _est _eex _edt; do
[ -n "$_ename" ] || continue
_n=$(( _n + 1 ))
[ "$_n" -le "$EV_MAX_ROWS" ] || continue
printf '| %s | %s | %s | %s | %s | %s |\n' \
"$(cell "$_ets")" "$(cell "$_ekind")" "$(cell "$_ename")" \
"$(cell "$_est")" "$(cell "${_eex:--}")" "$(cell "${_edt:--}")" >>"$RD/d11.md"
done <"$RD/events-bad.tsv"
[ "$_n" -gt "$EV_MAX_ROWS" ] \
&& printf '\n本輪非 ok 事件共 %s 筆,上表只列前 %s 筆。完整原文在 `%s`。\n' \
"$_n" "$EV_MAX_ROWS" "$RD/events.raw" >>"$RD/d11.md"
else
printf '本輪沒有 status 不是 ok 的事件。\n' >>"$RD/d11.md"
fi
{
printf '\n#### 有 start 沒有配對的 end\n\n'
printf '這一節就是中止的證據。`start` 由 hook 免費記下,`end` 要技能自己寫,所以只有 `start` 的那一筆,代表那一支技能沒有跑到收尾那一步。\n\n'
} >>"$RD/d11.md"
if [ -s "$RD/events-unpaired.tsv" ]; then
{
printf '| 名稱 | 類別 | session | start 時間 | 已開著(秒) |\n'
printf '| --- | --- | --- | --- | ---: |\n'
} >>"$RD/d11.md"
_n=0
while IFS=' ' read -r _uname _ukind _usess _uts _uage; do
[ -n "$_uname" ] || continue
_n=$(( _n + 1 ))
[ "$_n" -le "$EV_MAX_ROWS" ] || continue
printf '| %s | %s | %s | %s | %s |\n' \
"$(cell "$_uname")" "$(cell "$_ukind")" "$(cell "$_usess")" \
"$(cell "$_uts")" "$(cell "$_uage")" >>"$RD/d11.md"
done <"$RD/events-unpaired.tsv"
[ "$_n" -gt "$EV_MAX_ROWS" ] \
&& printf '\n未配對的 `start` 共 %s 筆,上表只列前 %s 筆。\n' "$_n" "$EV_MAX_ROWS" >>"$RD/d11.md"
else
printf '本輪沒有開超過門檻又沒收尾的 `start`。\n' >>"$RD/d11.md"
fi
printf '\n未達門檻的 `start` 不列進上表,它們多半只是還在跑。跨輪繼續配對,紀錄留在 `%s`;不跨輪的話,跑超過一個巡檢週期的技能每一輪都會被報成中止。\n' \
"$EVENTS_OPEN" >>"$RD/d11.md"
if [ "$EV_BAD" -gt 0 ]; then
add_warn '有非 ok 的執行狀態事件'
add_pending "本輪有 $EV_BAD 筆執行狀態不是 ok 的事件" '執行狀態事件' '照明細表的名稱找那一支技能或 hook'
fi
if [ "$EV_UNPAIRED" -gt 0 ]; then
add_warn '有技能只有 start 沒有 end'
add_pending "本輪有 $EV_UNPAIRED 支技能只有 start 沒有 end,那幾輪中止了" '執行狀態事件' '照明細表的名稱重跑那一支技能'
fi
if [ "$EV_TOTAL" -gt 0 ]; then
D11_STATUS=ok
else
D11_STATUS=empty
printf '\n來源讀得到,本輪沒有新事件(`drain` 回 3)。那是正常狀態,不是失敗:多數輪次本來就沒有新的技能或 hook 跑過。\n' >>"$RD/d11.md"
fi
return 0
}
# --- 待辦簿筆數(只供目錄頁那一列用)---
count_tasks() {
@@ -646,9 +894,9 @@ compose() {
printf '| 巡檢時間 | %s |\n' "$AT"
printf '| 觸發方式 | %s |\n' "$TRIGGER"
printf '| 本輪判定 | %s |\n' "$VERDICT"
printf '| 本輪項目 | 四項:D-01 使用統計、D-04 版本與重啟閘門、D-07 階段鎖與工作包鎖、D-09 心跳自述。成功 %s 項、失敗 %s 項 |\n' "$OK_COUNT" "$FAIL_COUNT"
printf '| 本輪項目 | 五項:D-01 使用統計、D-04 版本與重啟閘門、D-07 階段鎖與工作包鎖、D-09 心跳自述、D-11 執行狀態事件。成功 %s 項、失敗 %s 項 |\n' "$OK_COUNT" "$FAIL_COUNT"
printf '| 讀不到的來源 | %s |\n' "$(cell "${FAILED_SOURCES:-無}")"
# 警示來源緊接在讀不到的來源後面:兩列語意相近,而且四項讀取全部成功、判定卻是警示
# 警示來源緊接在讀不到的來源後面:兩列語意相近,而且各項讀取全部成功、判定卻是警示
# 時,這一塊裡只有這一列講得出原因,跟摘要表那一欄是同一個理由。
printf '| 警示來源 | %s |\n' "$(cell "${WARN_SOURCES:-無}")"
if [ "$LOCK_BROKEN" -eq 1 ]; then
@@ -657,6 +905,7 @@ compose() {
printf '\n'
cat "$RD/d09.md"; printf '\n'
cat "$RD/d01.md"; printf '\n'
cat "$RD/d11.md"; printf '\n'
printf '### hook 執行期錯誤\n\n'
printf '**這一輪不做這一項。** D-02 hook 錯誤巡檢還沒實作,這一節沒有資料不代表沒有 hook 錯誤。要現在查就跑 `/jsc-hooks:hooks-install` 的錯誤掃描,或直接跑 `jsc-hooks` 的 `tools/scan-hook-errors.sh`。\n\n'
cat "$RD/d04.md"; printf '\n'
@@ -672,8 +921,10 @@ compose() {
} >"$RD/latest.md"
# 摘要表的那一列。欄位刻意只有五個,一列要能一眼看完,才看得出是從哪一輪開始壞的。
# 「警示來源」那一欄不能省:四項讀取全部成功、但讀到的內容有警示時,判定是警示而成敗欄
# 是 4/4,沒有這一欄的話,看的人不知道警示哪來。
# 「警示來源」那一欄不能省:各項讀取全部成功、但讀到的內容有警示時,判定是警示而成敗欄
# 是滿分,沒有這一欄的話,看的人不知道警示哪來。
# 欄名寫「各項成敗」而不寫項數:巡檢項目會增加,欄名寫死數字就要跟著改,而舊頁那些列的
# 欄名不會跟著改,同一張表就會有兩種欄名。
printf '| %s | %s | %s/%s | %s | %s |\n' \
"$AT" "$VERDICT" "$OK_COUNT" "$ITEM_TOTAL" "$PEND_COUNT" \
"$(cell "${WARN_SOURCES:-無}")" >"$RD/summary-row.md"
@@ -682,7 +933,7 @@ compose() {
{
printf '## 近 24 輪摘要\n\n'
printf '一輪一列,最新的在最上面,超過 24 列就丟掉最舊的那一列。\n\n'
printf '| 巡檢時間 | 本輪判定 | 四項成敗 | 待人處理 | 警示來源 |\n'
printf '| 巡檢時間 | 本輪判定 | 各項成敗 | 待人處理 | 警示來源 |\n'
printf '| --- | --- | --- | ---: | --- |\n'
cat "$RD/summary-row.md"
} >"$RD/summary.md"
@@ -697,7 +948,7 @@ compose() {
printf '> 目錄頁 `MONITOR_CONTENTS` 在 `JSC_WIKI_REPO_CONTENTS` 解出的專用存取庫,和這頁不同庫。\n'
printf '> 那一頁只更新自己那一列,別台機器的列一個字都不動,寫入交給 `jsc-gitea/tools/wiki-contents.sh upsert`。\n\n'
printf '```mermaid\nflowchart LR\n'
printf ' A[巡檢一輪] --> B[收攏四項結果]\n'
printf ' A[巡檢一輪] --> B[收攏各項結果]\n'
printf ' B --> C[讀回舊頁]\n'
printf ' C --> D[換掉最新一輪那一塊]\n'
printf ' D --> E[本輪摘要列插到表格最上面,截到 24 列]\n'
@@ -783,8 +1034,10 @@ case "$CMD" in
d01
d04
d07
d11
count_tasks
tally "$D01_STATUS"; tally "$D04_STATUS"; tally "$D07_STATUS"; tally "$D09_STATUS"
tally "$D11_STATUS"
HASH=''
if _hi=$(find_tool gitea tools/hash-id "${JSC_ASSIST_HASH_ID:-}"); then
@@ -806,12 +1059,19 @@ case "$CMD" in
printf 'item=D-04 status=%s rc=%s note=%s\n' "$D04_STATUS" "$D04_RC" "$D04_NOTE"
printf 'item=D-07 status=%s rc=%s note=%s\n' "$D07_STATUS" "$D07_RC" "$D07_NOTE"
printf 'item=D-09 status=%s rc=%s note=%s\n' "$D09_STATUS" "$D09_RC" "$D09_NOTE"
printf 'item=D-11 status=%s rc=%s note=%s\n' "$D11_STATUS" "$D11_RC" "$D11_NOTE"
printf 'verdict=%s\n' "$VERDICT"
printf 'failed_sources=%s\n' "${FAILED_SOURCES:-無}"
printf 'warn_sources=%s\n' "${WARN_SOURCES:-無}"
printf 'tasks_total=%s\n' "$TASKS_TOTAL"
printf 'tasks_failing=%s\n' "$TASKS_FAILING"
printf 'pending=%s\n' "$PEND_COUNT"
printf 'events_total=%s\n' "$EV_TOTAL"
printf 'events_bad=%s\n' "$EV_BAD"
printf 'events_unpaired=%s\n' "$EV_UNPAIRED"
printf 'events_running=%s\n' "$EV_RUNNING"
printf 'events_rotated=%s\n' "$EV_ROTATED"
printf 'events_file=%s\n' "$RD/events.raw"
printf 'latest_file=%s\n' "$RD/latest.md"
printf 'summary_file=%s\n' "$RD/summary.md"
printf 'summary_row_file=%s\n' "$RD/summary-row.md"
+5
View File
@@ -440,9 +440,14 @@ print_allow_rules() {
# 少了規則就會停在權限詢問,而那一輪沒有人可以按同意。
# link-check.sh 同理:兩次寫入前都要先驗連結,少了這一條,驗證那一步就停在權限詢問,那一輪
# 什麼都寫不成。它排在寫入之前,所以擋住它等於整輪報廢。
# report-status.sh 是執行狀態事件那一支,這一輪會直接叫它兩次以上:排空與輪替由 patrol.sh
# 代跑(那是已放行指令的子行程,不會再問一次),但收尾那一筆 skill-end 是技能自己用 Bash 叫的,
# 那一次就要這一條規則。少了它,那一輪會停在最後一步的權限詢問,而排程那一輪沒有人可以按
# 同意:那一輪的收尾事件寫不出去,start 永遠配不到 end,下一輪就把一輪其實做完的巡檢報成中止。
for _s in "$CURRENT/jsc-assist/tools/schedule.sh" \
"$CURRENT/jsc-assist/tools/patrol.sh" \
"$CURRENT/jsc-hooks/hooks/heartbeat.sh" \
"$CURRENT/jsc-hooks/tools/report-status.sh" \
"$CURRENT/jsc-gitea/tools/gitea.sh" \
"$CURRENT/jsc-gitea/tools/wiki-contents.sh" \
"$CURRENT/jsc-gitea/tools/link-check.sh"; do