Files
hooks/tools/report-error.sh
T
jiantw83 02caedf91e feat(異常目錄頁): 索引改成 H2 區塊條列並委派共用工具
What:
異常目錄頁的版面從 markdown 表格改成一筆異常一個 H2 區塊,標題就是那一筆的異常頁頁名,時間、頁名、存取庫名稱、觸發 hook、退出碼、摘要六個欄位改成標題底下的一層條列,頁上不再留任何表格。失敗回報腳本不再自己讀回舊頁、附加新列、整頁寫回,改成只組出自己那一個區塊,交給 jsc-gitea 的目錄頁工具做讀回、比對與整頁寫回。範本、技能敘述、行為清單與說明文件一併對齊。

Why:
目錄頁有十幾份,各自在自己的腳本裡寫一套「讀得回舊內容才寫」的判斷,錯一次就少一筆紀錄,而且每一頁長出來的樣子都不一樣。版面與寫入語意收回一份正本之後,改一次全部跟著改。表格欄位遇到換行或半形豎線還會被切斷,條列沒有這個問題。

How:
失敗回報腳本移除目錄專用存取庫的解析與整頁組裝,改為組出區塊檔之後呼叫共用工具的 upsert,並依它的結束碼分流:3 是目錄頁的存取庫沒設定,找不到那支腳本也走同一條,兩者都只寫異常頁、跳過目錄頁、仍回 0;其餘非零一律以 4 回報。摘要不再替換半形豎線,改成把換行併成一行。傳進去的欄位序號 2 指舊表格版持有內容頁連結的那一欄,只供舊頁自動轉條列時取標題用。

Who:
使用 jsc-hooks 失敗回報流程的操作者,以及所有讀異常目錄頁追問題的人。
2026-09-02 18:01:33 +08:00

240 lines
13 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env sh
# report-error.sh — 失敗回報流程:把一筆 hook 或工具異常寫成 wiki 的 ERROR_{HASH},
# 並在異常目錄頁附上一個索引區塊。頁面內容套用 templates/ 的兩份範本,
# 範本是文案的唯一來源,本腳本只填欄位;真正寫進 wiki 前,還會先走
# Gitea 寫入確認。
# 目錄頁的讀回、比對與整頁寫回一律交給 jsc-gitea 的 tools/wiki-contents.sh,本腳本只組出
# 自己那一個區塊。目錄頁版面只留一份正本,十幾個目錄頁才不會各長一種樣子。
#
# 用法:
# report-error.sh --hook {名稱} --exit {碼} --summary {摘要}
# [--repo {owner}/{repo}] [--cli {名稱}] [--session {id}]
# [--source {stdin|env|command}] [--symptom {現象}]
# [--cause {可能原因}] [--action {處理結果}]
# 相關輸出(stdout/stderr 摘要)由標準輸入讀入,可省略。
#
# 輸出:
# 成功印出「{頁名} {網址}」一行。網址在異常頁寫成功之後才取,頁名的 hash 帶時間戳,
# 每次都是全新的頁,寫之前查一定是 404。取不到網址時只印頁名,原因走 stderr,仍然 exit 0。
# 下列四種情形安靜降級:不寫任何頁、不輸出任何內容、exit 0。回報失敗不該再變成一次失敗。
# 1. 找不到 gitea.sh
# 2. 解析不出異常頁的 wiki 存取庫(JSC_WIKI_REPO_ERROR 與 JSC_WIKI_REPO 都沒設)
# 3. 算不出 HASH(hash-id 失敗或回空字串)
# 4. 建不出暫存檔(mktemp 失敗)
# 異常頁與目錄頁分屬兩個存取庫,各解各的:解不出異常頁的存取庫就整支降級;解得出
# 異常頁、只解不出目錄頁的存取庫(wiki-contents.sh 回 3),就只寫異常頁、跳過目錄頁
# 更新,印出頁名,仍然 exit 0。找不到 wiki-contents.sh 也走同一條降級路。
# 一份寫得成的異常紀錄,不該因為目錄頁沒地方放就整份丟掉。
# 寫入 wiki 失敗才以 exit 4 回報,訊息走 stderr。
#
# 連結:
# 目錄頁那個區塊裡的「頁名」那一條指向異常頁,一律寫成 [{文字}]({連結}),網址取
# jsc-gitea 的 gitea.sh wiki-url,不自己組路徑。
# 寫入前先把那個網址交給 jsc-gitea 的 link-check.sh,結束碼 0 才把連結寫進那一條。
# 驗不過就只留純文字頁名:那一條照寫、異常頁照寫、結束碼照舊。這一段一律不改結束碼,
# 本腳本是失敗回報路徑,回報失敗不該再變成一次失敗。
#
# 結束碼: 0=已寫入異常頁並印出頁名(取得網址就一併印出),或以上列四種安靜降級原因之一
# 結束、沒有寫出任何頁也沒有任何輸出——回報失敗不該再變成一次失敗
# 2=用法錯誤(缺 --hook 或 --summary)
# 4=寫入 wiki 失敗(異常頁與索引目錄頁,任一支寫不進去就算),或目錄頁的舊內容
# 讀不回來(wiki-contents.sh 回 7 金鑰失效、8 其他 API 失敗)而放棄寫入;
# 訊息走 stderr。讀不回來就不寫,是為了不拿範本蓋掉一份還在的目錄頁
# 註: 本檔以 `. "$ROOT/hooks/lib.sh"` 載入共用函式,沒有接 `|| true`。lib.sh 讀不到時 sh 會
# 就地結束並回 2,跟用法錯誤同碼;分不出是哪一種時,先確認 hooks/lib.sh 在不在。
#
# 頁名:
# ERROR_{HASH},HASH 取「{owner}/{repo} {hook} {時間}」的 SHA-1 完整 40 碼大寫十六進位
# (共用 hash 規則)。三段以空白相連當 hash 輸入。
# 時間放進 hash:同一種失敗再發生時要另開新頁,不覆寫舊紀錄。
# 索引目錄頁的頁名固定,不帶 HASH。
#
# 誰來呼叫:
# 由操作者手動執行,或由技能步驟執行(`jsc-hooks:hooks-install` 在 wire-cli.sh 回報
# status=failed 時呼叫)。**不接在失敗的 hook 上自動觸發**:hook 一律安靜 exit 0,
# 而且自我回報要走網路寫 wiki,失敗的 hook 再去回報自己會疊出迴圈。
set -u
HERE=$(cd "$(dirname "$0")" && pwd)
ROOT=$(cd "$HERE/.." && pwd)
. "$ROOT/hooks/lib.sh"
STDIN_JSON="" # 本腳本的標準輸入是錯誤輸出摘要,不是 JSON
hook=""; code=""; summary=""; repo=""; cli=""; session=""
source_kind=""; symptom=""; cause=""; action=""
while [ $# -gt 0 ]; do
case "$1" in
--hook) hook="${2:-}"; shift 2 ;;
--exit) code="${2:-}"; shift 2 ;;
--summary) summary="${2:-}"; shift 2 ;;
--repo) repo="${2:-}"; shift 2 ;;
--cli) cli="${2:-}"; shift 2 ;;
--session) session="${2:-}"; shift 2 ;;
--source) source_kind="${2:-}"; shift 2 ;;
--symptom) symptom="${2:-}"; shift 2 ;;
--cause) cause="${2:-}"; shift 2 ;;
--action) action="${2:-}"; shift 2 ;;
*) shift ;;
esac
done
if [ -z "$hook" ] || [ -z "$summary" ]; then
echo "用法:report-error.sh --hook {名稱} --exit {碼} --summary {摘要} [...]" >&2
exit 2
fi
gsh=$(jsc_gitea_sh) || exit 0
# 異常頁與索引目錄頁落在兩個不同的存取庫,各解各的:異常頁走自己的型別,目錄頁走目錄專用
# 型別。異常頁的存取庫解不出來就整支降級,連異常都沒地方寫,做下去也沒意義。
wrepo=$(sh "$gsh" wiki-repo ERROR 2>/dev/null) || exit 0
[ -n "$wrepo" ] || exit 0
# 目錄頁的存取庫不在這裡解:讀回、比對、整頁寫回都由 wiki-contents.sh 做,它自己解目錄專用
# 存取庫,這邊再解一次就會有兩份規則。解不出來時它回 3,本腳本照原本的語意降級。
# 存取庫名稱未指定就取工作目錄的 origin(只用來標記異常屬於哪個存取庫)
if [ -z "$repo" ]; then
origin=$(git config --get remote.origin.url 2>/dev/null || true)
# 尾綴的 .git 在這裡剝掉:hash 工具刻意不做輸入正規化,同一份輸入要算出同一個 HASH,
# 正規化就是呼叫端的責任。帶不帶 .git 會算出兩個不同的頁,同一個存取庫就分裂成兩份紀錄。
repo=$(printf '%s' "$origin" \
| sed -n 's#.*[/:]\([^/]*\)/\([^/]*\)$#\1/\2#p' | sed 's/\.git$//')
fi
[ -n "$repo" ] || repo="-"
[ -n "$cli" ] || cli=$(cli_name)
[ -n "$session" ] || session=$(session_id)
[ -n "$code" ] || code="-"
[ -n "$source_kind" ] || source_kind="command"
[ -n "$symptom" ] || symptom="$summary"
[ -n "$cause" ] || cause="待查"
[ -n "$action" ] || action="待處理"
ts=$(date +'%Y-%m-%d %H:%M:%S')
ticket_ts=$(date +'%Y%m%d_%H%M%S')
if [ -t 0 ]; then detail=""; else detail=$(cat 2>/dev/null | tr '\n' ' ' | cut -c1-500); fi
[ -n "$detail" ] || detail="(無)"
# 相關輸出落在異常頁的表格欄位裡,半形 | 會把欄位切斷,改成全形
detail=$(printf '%s' "$detail" | sed 's/|/|/g')
# 摘要在目錄頁是一整條條列,換行會把一條拆成兩行,先併成一行
summary=$(printf '%s' "$summary" | tr '\n' ' ')
hash=$(sh "$gsh" hash-id "$repo $hook $ts" 2>/dev/null) || exit 0
[ -n "$hash" ] || exit 0
page="ERROR_$hash"
# sed 取代值要先轉義:& 與分隔字元 | 會被 sed 當語法,換行會整行斷掉
esc() { printf '%s' "$1" | tr '\n' ' ' | sed 's/[\\&|]/\\&/g'; }
fill() { sed "s|$1|$(esc "$2")|g"; }
tmp_page=$(mktemp) || exit 0
tmp_entry=$(mktemp) || { rm -f "$tmp_page"; exit 0; }
trap 'rm -f "$tmp_page" "$tmp_entry"' EXIT
fill '{HASH}' "$hash" < "$ROOT/templates/error-page.md" \
| fill '{yyyy-MM-dd HH:mm:ss}' "$ts" \
| fill '{owner}/{repo}' "$repo" \
| fill '{cli}' "$cli" \
| fill '{session_id}' "$session" \
| fill '{hook_name}' "$hook" \
| fill '{exit_code}' "$code" \
| fill '{error_summary}' "$summary" \
| fill '{現象描述}' "$symptom" \
| fill '{可能原因}' "$cause" \
| fill '{處理方式}' "$action" \
| fill '{stdin / env / command}' "$source_kind" \
| fill '{stdout / stderr 摘要}' "$detail" \
| fill '{yyyyMMdd}_{HHmmss}' "$ticket_ts" > "$tmp_page"
# 異常頁先寫,網址後取。頁名的 hash 帶時間戳,每次回報都是一個全新的頁,寫進去之前查網址
# 一定是 404,拿到的必然是空字串;目錄頁那一條會變成沒有連結的死字,stdout 也少一半。
if ! sh "$gsh" wiki-put "$wrepo" "$page" "$tmp_page" >/dev/null 2>&1; then
echo "[jsc] 寫入 $page 失敗($wrepo)。" >&2
exit 4
fi
# 目錄頁那個區塊的「頁名」那一條指向異常頁,連結一律寫成 [{文字}]({連結}),網址取
# gitea.sh wiki-url。
# wiki 自己那種雙中括號寫法只在同一個 wiki 裡解得開,寫錯不會報錯,畫面上看起來像正常
# 文字或死連結,巡不到也修不了。
# 網址取不到不算失敗:異常頁已經寫成功了,只是這一條少一個連結。這裡把原因記下來走 stderr,
# 結束碼照舊——安靜降級仍是 exit 0,回報失敗不該再變成一次失敗。
url=$(sh "$gsh" wiki-url "$wrepo" "$page" 2>/dev/null)
url_code=$?
url_note=''
if [ "$url_code" -ne 0 ]; then
url=''
case "$url_code" in
4) url_note='頁面查不到(wiki-url 回 4),寫入後尚未生效' ;;
5) url_note='回應裡沒有 html_url(wiki-url 回 5)' ;;
7) url_note='金鑰失效或權限不足(wiki-url 回 7)' ;;
*) url_note="wiki-url 結束碼 $url_code" ;;
esac
echo "[jsc] 取不到 $page 的網址:$url_note。目錄頁那一條與輸出只留頁名。" >&2
fi
emit() { # 異常頁已經寫成功,頁名一定要印;網址取不到就只印頁名,不印一個空欄位
if [ -n "$url" ]; then printf '%s %s\n' "$page" "$url"; else printf '%s\n' "$page"; fi
}
# 連結先驗證連得到,才寫進目錄頁那一條。沒驗過的連結寫進去,異常頁一樣會在目錄頁長出
# 死連結,而目錄頁是別人查問題的入口。驗不過就只留純文字頁名,那一條照寫。
link_text="$page"
if [ -n "$url" ]; then
# link-check.sh 與 gitea.sh 同一個 tools 目錄,路徑直接由已經解出來的那一支推得,
# 不另寫一套搜尋,兩邊才不會一支解到開發版面、一支解到安裝版面。
lcs="$(dirname "$gsh")/link-check.sh"
if [ ! -f "$lcs" ]; then
echo "[jsc] 找不到 $lcs,這一條的連結沒驗過,只留頁名;$page 已建立。" >&2
else
sh "$lcs" "$url" >/dev/null 2>&1
lc_code=$?
case "$lc_code" in
0) link_text=$(printf '[%s](%s)' "$page" "$url") ;;
# 7 是金鑰失效,不是死連結。金鑰過期時私有存取庫的回應與「頁不存在」分不出來,
# 把它當成死連結就會連還在的頁一起判死。
7) echo "[jsc] 連結驗證遇上金鑰失效(link-check.sh 回 7),不判成死連結,這一條只留頁名;$page 已建立。" >&2 ;;
3) echo "[jsc] GITEA_HOST 未設定(link-check.sh 回 3),連結沒驗過,這一條只留頁名;$page 已建立。" >&2 ;;
*) echo "[jsc] 連結驗不過(link-check.sh 結束碼 $lc_code),這一條只留頁名;$page 已建立。" >&2 ;;
esac
fi
fi
# 目錄頁上這一筆是一個 H2 區塊:標題就是異常頁頁名,欄位一行一條,順序與範本的示範區塊
# 一致。標題不放連結也不放網址——頁名只由存取庫名稱、hook 與時間決定,換主機或改存取庫
# 都動不到它,比對鍵才找得到既有那一筆。
{
printf '## %s\n\n' "$page"
printf -- '- 時間:%s\n' "$ts"
printf -- '- 頁名:%s\n' "$link_text"
printf -- '- 存取庫名稱:%s\n' "$repo"
printf -- '- 觸發 hook:%s\n' "$hook"
printf -- '- 退出碼:%s\n' "$code"
printf -- '- 摘要:%s\n' "$summary"
} > "$tmp_entry"
# 目錄頁交給 wiki-contents.sh:它負責解目錄專用存取庫、讀回舊頁、比對 H2 標題找既有區塊、
# 附加或整塊換掉,再整頁寫回,還會把舊的表格頁轉成條列。頁上每一個區塊都是別人回報的異常,
# 那套「讀得回舊內容才寫」的判斷只留一份正本,才不會每個目錄頁各寫一套、錯一次少一筆紀錄。
# 傳進去的那個 2 是 key-col:舊表格版目錄頁裡持有身分的欄位序號。舊版第 2 欄是頁名,
# 舊頁自動轉條列時要靠它取 H2 標題;頁面已經是條列格式時這個參數用不到。
# 路徑由已經解出來的 gitea.sh 推得,與 link-check.sh 同一套做法,三支同一個 tools 目錄。
wcs="$(dirname "$gsh")/wiki-contents.sh"
if [ ! -f "$wcs" ]; then
echo "[jsc] 找不到 $wcs,只寫異常頁,跳過目錄;$page 已建立($wrepo)。" >&2
emit
exit 0
fi
sh "$wcs" upsert ERROR 2 "$page" "$tmp_entry" "$ROOT/templates/error-contents.md" >/dev/null
wc_code=$?
case "$wc_code" in
0) ;;
# 3 是目錄頁的存取庫沒設定。異常頁已經寫成功,一份寫得成的異常紀錄不該因為索引沒地方放
# 就整份丟掉,所以只跳過目錄頁,結束碼照舊回 0。
3) echo "[jsc] 目錄頁的 wiki 存取庫解不出來(wiki-contents.sh 回 3),只寫異常頁,跳過目錄;$page 已建立($wrepo)。" >&2 ;;
# 其餘結束碼都代表這一筆沒進到目錄頁:7 金鑰失效、8 其他 API 失敗都是舊內容未知而中止,
# 1 是組不出頁面內容或寫入失敗,2 是用法錯誤,4 是頁面不存在又沒收到範本。
*)
echo "[jsc] 目錄頁寫入失敗(wiki-contents.sh 結束碼 $wc_code),$page 已建立。" >&2
exit 4 ;;
esac
emit