Files
hooks/tools/report-error.sh
T
jiantw83 f4871afd19 feat(wiki): 異常目錄頁改走專用存取庫,並修正目錄列連結恆空
What:ERROR_CONTENTS 改由 wiki-repo CONTENTS 解析,ERROR_{HASH} 仍走
wiki-repo ERROR,兩者是兩個不同的存取庫。目錄列的連結改用 wiki-url 的絕對網址。
註解掃描的頁面編號樣式補上 40 碼與 H 加 7 碼兩種形狀。

Why:目錄列的網址原本在異常頁寫入之前就取,而頁名的雜湊帶時間戳、每次都是全新頁,
那時查一定是 404,又被吞掉,所以那一格一直都是空的。註解掃描原本只收 8 碼純十六進位,
舊演算法有十三個首碼會改寫成 H 開頭,等於對絕大多數舊頁編號漏偵測。

How:降級語意分兩層——異常頁的存取庫解不出來就整支安靜降級,只有目錄頁解不出來就
只寫異常頁、跳過索引,兩種都維持 exit 0。「只有 exit 4 才准建新頁、7 與 8 一律中止」
那段原樣保留,那是防止把金鑰失效讀成頁面不存在、拿範本蓋掉整頁既有列。

Who:jsc-hooks
2026-09-02 11:30:49 +08:00

208 lines
10 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},
# 並在 ERROR_CONTENTS 附上一列索引。頁面內容套用 templates/ 的兩份範本,
# 範本是文案的唯一來源,本腳本只填欄位;真正寫進 wiki 前,還會先走
# Gitea 寫入確認。
#
# 用法:
# 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 失敗)
# 異常頁與目錄頁分屬兩個存取庫,各解各的:解不出異常頁的存取庫就整支降級;解得出
# 異常頁、只解不出目錄頁的存取庫,就只寫異常頁、跳過目錄頁更新,印出頁名,仍然 exit 0。
# 一份寫得成的異常紀錄,不該因為目錄頁沒地方放就整份丟掉。
# 寫入 wiki 失敗才以 exit 4 回報,訊息走 stderr。
#
# 結束碼: 0=已寫入異常頁並印出頁名(取得網址就一併印出),或以上列四種安靜降級原因之一
# 結束、沒有寫出任何頁也沒有任何輸出——回報失敗不該再變成一次失敗
# 2=用法錯誤(缺 --hook 或 --summary)
# 4=寫入 wiki 失敗(異常頁與索引目錄頁,任一支寫不進去就算),或目錄頁的舊內容
# 讀不回來(wiki-get 回 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
# 目錄頁的存取庫解不出來不算失敗:異常頁照寫,只跳過目錄頁更新,仍然 exit 0。
# 一份寫得成的異常紀錄,不該因為索引沒地方放就整份丟掉。
crepo=$(sh "$gsh" wiki-repo CONTENTS 2>/dev/null || true)
# 存取庫名稱未指定就取工作目錄的 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="(無)"
# 摘要與相關輸出都落在 markdown 表格欄位裡,半形 | 會把欄位切斷,改成全形
detail=$(printf '%s' "$detail" | sed 's/|/|/g')
summary=$(printf '%s' "$summary" | sed 's/|/|/g')
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_list=$(mktemp) || { rm -f "$tmp_page"; exit 0; }
trap 'rm -f "$tmp_page" "$tmp_list"' 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
# 目錄頁與異常頁分屬兩個存取庫,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
}
row=$(printf '| %s | [%s](%s) | %s | %s | %s | %s |' \
"$ts" "$hook 異常 $ts" "$url" "$repo" "$hook" "$code" "$summary")
build_contents() { # 用範本建一份全新的目錄頁;只有確定舊頁不存在時才可以呼叫
fill '{yyyy-MM-dd HH:mm:ss}' "$ts" < "$ROOT/templates/error-contents.md" \
| fill '{error url}' "$url" \
| fill '{error title}' "$hook 異常 $ts" \
| fill '{owner}/{repo}' "$repo" \
| fill '{hook_name}' "$hook" \
| fill '{exit_code}' "$code" \
| fill '{error_summary}' "$summary" > "$tmp_list"
}
if [ -z "$crepo" ]; then
echo "[jsc] 目錄頁的 wiki 存取庫解不出來,只寫異常頁,跳過目錄;$page 已建立($wrepo)。" >&2
emit
exit 0
fi
# 異常目錄頁一律附加,不整頁覆蓋。頁上每一列都是別人回報的異常,本腳本沒有從別處讀過
# 那些列,所以先把舊頁讀回來、把新列附在文末(最新一筆在最後),再整頁寫回。
# 這個語意完全靠「讀得回舊內容」撐著,因此依 wiki-get 的結束碼分流:
# 0 → 讀到既有內容,附加新列(讀得到但整頁是空的,沒有既有列會被蓋掉,套範本才安全)
# 4 → 頁面真的還不存在,只有這個碼可以用範本建立新頁
# 7 → 金鑰失效或權限不足,舊內容未知,放棄目錄頁寫入
# 8 → 其他 API 失敗,舊內容一樣未知,處置同 7
# 為什麼 7 與 8 不能當成「頁面不存在」:拿範本蓋掉一份讀不回來的目錄頁,等於刪光所有既有
# 列,而 wiki-put 不做合併、也不留備份,蓋掉就救不回來。
sh "$gsh" wiki-get "$crepo" ERROR_CONTENTS > "$tmp_list" 2>/dev/null
get_code=$?
case "$get_code" in
0) if [ -s "$tmp_list" ]; then printf '%s\n' "$row" >> "$tmp_list"; else build_contents; fi ;;
4) build_contents ;;
*)
echo "[jsc] 讀取目錄頁失敗($crepo,wiki-get 結束碼 $get_code),舊內容未知,不寫目錄頁;$page 已建立。" >&2
exit 4 ;;
esac
if ! sh "$gsh" wiki-put "$crepo" ERROR_CONTENTS "$tmp_list" >/dev/null 2>&1; then
echo "[jsc] 寫入目錄頁失敗($crepo),$page 已建立。" >&2
exit 4
fi
emit