fix(lib): 跨外掛路徑解析改實體解析,修好版本閘門

版本閘門對 11 個 domain 全部回「查詢失敗」,等於完全失效——它擋不下任何版本落後的技能呼叫,而且是無聲的:report 照印表格,只是每一列都寫查詢失敗。

根因是 jsc_gitea_sh 回傳的路徑帶著 ..,而那些 .. 要穿過 current/jsc-hooks 這條符號連結。兩種解析方式對它的答案不同:核心與 [ -f ] 用實體解析、跟著連結走,判定檔案存在;shell 的 cd 用邏輯解析、純文字消去 ..,落到一個不存在的目錄。所以 [ -f ] 檢查通過、路徑交了出去,gitea.sh 的 cd 卻失敗,回結束碼 2 與空輸出,呼叫端就判成查不到。

新增 jsc_abs_path,用 cd -P 加 pwd -P 把路徑正規化成不含 .. 的實體路徑。挑這個做法是因為兩者都是 shell 內建,不必在 PATH 上找執行檔——這些函式會在 cron 那種只剩幾段 PATH 的環境下跑,少一個外部相依就少一個解不出來的理由。jsc_gitea_sh 的四條候選改成尾端統一正規化,正規化失敗就退回原樣路徑,「找得到」的判準不變。

wire-cli.sh 的 HERE 與 ROOT 一併改成實體解析。那是同一個根因的另一種發作方式:ROOT 會被 ln -sfn 當成目標,而這支腳本常常就是經由那條連結被叫起來的,邏輯解析會讓 ROOT 等於連結自己,連結被改成指向自己,全機器 hook 一起失效。這件事實際發生過。原本靠兩道防線擋著:事後的 [ -f ] 檢查,以及文件要求呼叫端先解出實體根目錄。前者要等連結已經被寫壞才攔得到,後者靠人記得。改成實體解析之後這個失敗模式不可能成立。

行為契約第 10 列跟著改:從實體根目錄跑 wire-cli.sh 的規定保留,但性質從必要條件降成多一層保險,並寫明保險為什麼還值得買——舊版腳本還在別的機器上跑。

驗證用同形佈局做:暫存區搭一套一樣形狀的連結農場,先塞原版重現失敗、再塞改版確認修好。真實環境的連結與快取全程沒有動過。
This commit is contained in:
2026-09-03 12:19:27 +08:00
parent f0fd92eb33
commit 95c7bb1eee
3 changed files with 57 additions and 17 deletions
+48 -14
View File
@@ -229,27 +229,61 @@ cli_name() {
else printf 'unknown'; fi
}
# 把一個檔案路徑正規化成不含 `..` 的實體路徑。解不出來就回傳 1。
#
# 為什麼一定要正規化:路徑裡的 `..` 一旦要穿過符號連結,兩種解法會給出不同的答案。
# 核心與 `[ -f ]` 走實體解析:先跟著連結走到目標,再從目標往上退。
# shell 的 `cd` 走邏輯解析:把 `..` 當純文字消去,退回的是連結自己的上層目錄。
# 找別的 plugin 是靠自己的位置往上退幾層再往下找,而安裝版面的腳本目錄正是經由一條符號
# 連結被叫到的,退層數一超過連結目標底下的深度就會踩到這個差異:這裡的 `[ -f ]` 說檔案
# 在、把路徑交出去,被呼叫的腳本自己 `cd` 過去卻找不到那個目錄,回一個空輸出與非零結束
# 碼。呼叫端只看得到「查詢失敗」,看不出是路徑寫法的問題,於是整道閘門無聲失效。
#
# 為什麼用 `cd -P` 加 `pwd -P` 而不是 readlink:這兩個都是 shell 內建,不必在 PATH 上找
# 外部執行檔。這些函式會在 cron 那種只剩幾段 PATH 的環境下跑,少一個外部相依就少一個
# 解不出來的理由。`-P` 是逐段跟著連結走的那一種解法,跟核心的答案一致。
jsc_abs_path() { # $1=檔案路徑
[ -n "${1:-}" ] || return 1
_ap_dir=$(CDPATH= cd -P -- "$(dirname -- "$1")" 2>/dev/null && pwd -P) || return 1
[ -n "$_ap_dir" ] || return 1
case "$_ap_dir" in
*/) printf '%s%s\n' "$_ap_dir" "$(basename -- "$1")" ;;
*) printf '%s/%s\n' "$_ap_dir" "$(basename -- "$1")" ;;
esac
}
# 找出 jsc-gitea 的 tools/gitea.sh 絕對路徑。所有 gitea 操作一律經由它(技能準則),
# 不可自行拼 API 呼叫:token 取用與 tea 金鑰退回都寫在那支腳本裡。
# 找不到就回傳 1,由呼叫端安靜降級(hook 一律 exit 0,不中斷宿主 CLI)。
#
# 每一條候選路徑都先湊出來、最後統一過 jsc_abs_path 才交出去,理由見該函式的說明:
# 這裡的候選帶著 `..`,而那些 `..` 要穿過安裝版面的符號連結,交出去的原樣路徑
# 只有 `[ -f ]` 認得,被呼叫的腳本自己 `cd` 過去會失敗。正規化失敗時退回原樣路徑,
# 讓「找得到」這件事的判準不因為多了一道正規化而變嚴。
jsc_gitea_sh() {
if [ -n "${JSC_GITEA_TOOLS:-}" ] && [ -f "$JSC_GITEA_TOOLS/gitea.sh" ]; then
printf '%s\n' "$JSC_GITEA_TOOLS/gitea.sh"; return 0
fi
_c=""
_root="${CLAUDE_PLUGIN_ROOT:-$JSC_SCRIPT_DIR/..}"
if [ -n "${JSC_GITEA_TOOLS:-}" ] && [ -f "$JSC_GITEA_TOOLS/gitea.sh" ]; then
_c="$JSC_GITEA_TOOLS/gitea.sh"
fi
# 開發用的並排存取庫版面:{workspace}/hooks 旁邊就是 {workspace}/gitea
for _c in "$_root/../gitea/tools/gitea.sh" "$_root/../jsc-gitea/tools/gitea.sh"; do
[ -f "$_c" ] && { printf '%s\n' "$_c"; return 0; }
done
if [ -z "$_c" ]; then
for _p in "$_root/../gitea/tools/gitea.sh" "$_root/../jsc-gitea/tools/gitea.sh"; do
[ -f "$_p" ] && { _c="$_p"; break; }
done
fi
# 已安裝版面:每個 plugin 各有版本目錄,取排序最後的一份(通常即最新版)
_c=$(ls -d "$_root"/../../jsc-gitea/*/tools/gitea.sh \
"$_root"/../../gitea/*/tools/gitea.sh \
"$HOME"/.claude/plugins/cache/*/jsc-gitea/*/tools/gitea.sh 2>/dev/null \
| sort | tail -n1)
[ -n "$_c" ] && [ -f "$_c" ] && { printf '%s\n' "$_c"; return 0; }
_c=$(command -v gitea.sh 2>/dev/null || true)
[ -n "$_c" ] && { printf '%s\n' "$_c"; return 0; }
return 1
if [ -z "$_c" ]; then
_p=$(ls -d "$_root"/../../jsc-gitea/*/tools/gitea.sh \
"$_root"/../../gitea/*/tools/gitea.sh \
"$HOME"/.claude/plugins/cache/*/jsc-gitea/*/tools/gitea.sh 2>/dev/null \
| sort | tail -n1)
[ -n "$_p" ] && [ -f "$_p" ] && _c="$_p"
fi
[ -n "$_c" ] || _c=$(command -v gitea.sh 2>/dev/null || true)
[ -n "$_c" ] || return 1
_n=$(jsc_abs_path "$_c") && [ -n "$_n" ] && _c="$_n"
printf '%s\n' "$_c"
}
# 每個 CLI 代號對應的實際執行檔(antigravity 是 agy、kiro 是 kiro-cli,其餘同名)