fix(hooks): 補齊稽核缺失並修掉護欄失效
What:依 jsc-meta:skill-check 的稽核結果修正技能與工具——補上每個步驟的可檢核完成條件、 把留在內文的標準輸入輸出流程下放 tools/、修正查表與退碼路由造成的誤判。 Why:稽核發現這些缺失會讓技能在實際執行時走錯分支或靜默通過。 完成條件缺漏是最常被違反的一項;退碼誤判與查表錯誤則會讓良性狀況被當成失敗。 How:逐項對照 references/guidelines.md 的審核檢查清單修正,新增的工具都有 documented exit codes,並以真實執行驗證每條路徑。 Who:jsc-meta:skill-check 例行稽核(2026-08-25)。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,10 @@
|
||||
JSC_HOME="${JSC_HOME:-$HOME/.jsc}"
|
||||
mkdir -p "$JSC_HOME/sessions" "$JSC_HOME/usage" 2>/dev/null || true
|
||||
|
||||
# 呼叫端腳本所在目錄。source 不會改變 $0,所以這裡取到的是 hooks/ 或 tools/。
|
||||
JSC_SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" 2>/dev/null && pwd)
|
||||
JSC_SCRIPT_DIR="${JSC_SCRIPT_DIR:-.}"
|
||||
|
||||
# 讀完 stdin(可能為空;非阻塞宿主)
|
||||
read_stdin() {
|
||||
if [ -t 0 ]; then STDIN_JSON=""; else STDIN_JSON=$(cat 2>/dev/null || true); fi
|
||||
@@ -47,5 +51,37 @@ cli_name() {
|
||||
else printf 'unknown'; fi
|
||||
}
|
||||
|
||||
# 找出 jsc-gitea 的 tools/gitea.sh 絕對路徑。所有 gitea 操作一律經由它(技能準則),
|
||||
# 不可自行拼 API 呼叫:token 取用與 tea 金鑰退回都寫在那支腳本裡。
|
||||
# 找不到就回傳 1,由呼叫端安靜降級(hook 一律 exit 0,不中斷宿主 CLI)。
|
||||
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
|
||||
_root="${CLAUDE_PLUGIN_ROOT:-$JSC_SCRIPT_DIR/..}"
|
||||
# 開發用的並排存取庫版面:{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
|
||||
# 已安裝版面:每個 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
|
||||
}
|
||||
|
||||
# 每個 CLI 代號對應的實際執行檔(antigravity 是 agy、kiro 是 kiro-cli,其餘同名)
|
||||
cli_bin() { # $1=CLI 代號
|
||||
case "$1" in
|
||||
antigravity) printf 'agy' ;;
|
||||
kiro) printf 'kiro-cli' ;;
|
||||
*) printf '%s' "$1" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
now_epoch() { date +%s; }
|
||||
now_iso() { date -u +%Y-%m-%dT%H:%M:%SZ; }
|
||||
|
||||
+10
-4
@@ -35,15 +35,21 @@ stage_tags() { # $1=階段
|
||||
awk -F'\t' -v s="$1" '$1 == "stage" && $2 == s { print $3; exit }' "$TAGS_TSV"
|
||||
}
|
||||
|
||||
# 某模型的能力標籤。模型鍵與實際 id 雙向包含即視為同一家族
|
||||
#(例:表列 claude-haiku-4-5 對得上 claude-haiku-4-5-20251001);多筆命中取最長鍵。
|
||||
# 某模型的能力標籤。查法:先找完全相同的鍵,沒有才退回「表列鍵是實際 id 的前綴」
|
||||
# 之中最長的一筆(例:表列 claude-haiku-4-5 對得上 claude-haiku-4-5-20251001)。
|
||||
# 只認前綴這個方向。反向包含(實際 id 是表列鍵的前綴)會讓 gpt-5.x 命中更長的
|
||||
# gpt-5.x-mini,最長鍵勝出就把 mini 的標籤發給 gpt-5.x,把夠格的模型擋掉。
|
||||
model_tags() { # $1=模型 id
|
||||
[ -s "$TAGS_TSV" ] || return 0
|
||||
awk -F'\t' -v m="$1" '
|
||||
$1 == "model" && (index(m, $2) > 0 || index($2, m) > 0) {
|
||||
$1 == "model" && $2 == m { exact = $3 }
|
||||
$1 == "model" && $2 != m && substr(m, 1, length($2)) == $2 {
|
||||
if (length($2) > best_len) { best_len = length($2); best = $3 }
|
||||
}
|
||||
END { if (best != "") print best }
|
||||
END {
|
||||
if (exact != "") print exact
|
||||
else if (best != "") print best
|
||||
}
|
||||
' "$TAGS_TSV"
|
||||
}
|
||||
|
||||
|
||||
+11
-1
@@ -1,9 +1,16 @@
|
||||
#!/usr/bin/env sh
|
||||
# session-timer.sh — 記錄工作階段花費時間(供 jsc-log:worklog 取用)。
|
||||
# 用法:
|
||||
# session-timer.sh start # SessionStart:記錄起始時間
|
||||
# session-timer.sh start # SessionStart:記錄起始時間(已有紀錄就不動)
|
||||
# session-timer.sh restart # SessionStart:一律覆寫起始時間
|
||||
# session-timer.sh mark # Stop/SessionEnd:更新最後活動時間
|
||||
# session-timer.sh report [sid] # 印出 {sid} {seconds};無紀錄印 0
|
||||
#
|
||||
# start 與 restart 的差別在「同一個 session id 會不會重複開始」:
|
||||
# start 給 Claude 這種每個工作階段都有自己 session id 的 CLI。續接同一階段時
|
||||
# SessionStart 會再觸發一次,覆寫起始時間會讓花費時間歸零。
|
||||
# restart 給接不到 session id 的 CLI(kiro)。那些 CLI 的紀錄共用 default,
|
||||
# 不覆寫就會把上一個工作階段的起始時間算進來,花費時間虛胖。
|
||||
HERE=$(dirname "$0"); . "$HERE/lib.sh"
|
||||
read_stdin
|
||||
sid=$(session_id)
|
||||
@@ -11,6 +18,9 @@ case "${1:-mark}" in
|
||||
start)
|
||||
f="$JSC_HOME/sessions/$sid.start"
|
||||
[ -f "$f" ] || now_epoch > "$f" ;;
|
||||
restart)
|
||||
now_epoch > "$JSC_HOME/sessions/$sid.start"
|
||||
rm -f "$JSC_HOME/sessions/$sid.end" ;;
|
||||
mark)
|
||||
now_epoch > "$JSC_HOME/sessions/$sid.end" ;;
|
||||
report)
|
||||
|
||||
Regular → Executable
+180
-158
@@ -3,15 +3,22 @@
|
||||
#
|
||||
# 本機版本落後遠端發佈版本時擋下該次技能呼叫,並提示更新指令。
|
||||
#
|
||||
# 輸入:stdin JSON(Claude 格式)或環境變數,兩者都收。
|
||||
# 工具名 JSC_TOOL_NAME、TOOL_NAME、stdin 的 tool_name
|
||||
# 技能名 JSC_SKILL、SKILL、stdin 的 skill
|
||||
# 兩邊都拿不到就安靜降級 exit 0。
|
||||
#
|
||||
# 判準與取值:
|
||||
# - 比對對象是「遠端發佈版本」與「本機**實際載入**的版本」。
|
||||
# 實際載入版本要從 installed_plugins.json 的 installPath 讀該版目錄下的
|
||||
# plugin.json,不能只看註冊在 installed_plugins.json 的版本欄位——那兩者
|
||||
# 可能不同,只看註冊值會放過真正被載入的舊版。
|
||||
# 實際載入版本只認 installed_plugins.json 的 installPath 底下那份 plugin.json,
|
||||
# 不看註冊在 installed_plugins.json 的版本欄位——那兩者可能不同,註冊值比較新時
|
||||
# 會放過真正被載入的舊版。讀不到那份檔案就當查不到,安靜放行。
|
||||
# - 只擋落後。本機版本等於或超前遠端一律放行:開發技能組時本機本來就會
|
||||
# 超前 master,擋下去會讓維護者自己動不了。
|
||||
# - 遠端版本查不到(離線、站台維護、repo 改名)一律**擋**(fail-closed),
|
||||
# 避免「查不到就當作沒事」而讓落後版本靜靜跑下去。逃生門見下。
|
||||
# 超前預設分支,擋下去會讓維護者自己動不了。
|
||||
# - **只有「本機落後遠端」會擋**。查不到資料一律放行(exit 0):本機版本、
|
||||
# Gitea 站台、遠端版本全部來自 Claude 的 plugin 檔案與 Gitea API,沒裝
|
||||
# Claude 或離線的機器一筆都讀不到。那種情況擋下去,等於在沒有任何版本
|
||||
# 證據時停掉每一次技能呼叫,護欄變成故障點。
|
||||
#
|
||||
# 豁免(這些技能永遠放行):
|
||||
# jsc-cli:deploy 更新整組技能的入口,擋了就沒有任何方法更新,會死鎖
|
||||
@@ -22,84 +29,148 @@
|
||||
# 逃生門:JSC_VERSION_GUARD=off 完全略過檢查(離線工作時用)。
|
||||
#
|
||||
# 快取:$JSC_HOME/version-cache/{domain},單行「{版本} {epoch}」,
|
||||
# 預設 600 秒內不重查(JSC_VERSION_TTL 可調)。
|
||||
# 預設 600 秒內不重查(JSC_VERSION_TTL 可調)。hook 與 report 共用同一份。
|
||||
#
|
||||
# 另有一個非 hook 的子指令:
|
||||
# version-guard.sh report 把每個已安裝 jsc-* plugin 的版本比對印成 TSV,每行
|
||||
# 「{domain}<TAB>{本機}<TAB>{遠端}<TAB>{落後|最新|超前|查詢失敗}」,
|
||||
# 最後一行「behind<TAB>{落後個數}」。供 jsc-cli:deploy 判斷要不要
|
||||
# 把「更新」設成推薦選項。report 只讀不擋,永遠 exit 0。
|
||||
# 本機沒有 Claude 的 plugin 註冊檔時改印「noregistry<TAB>{路徑}」
|
||||
# 再接 behind 0:那代表這台機器無法做版本檢查,跟「全部最新」是兩件事。
|
||||
HERE=$(dirname "$0"); . "$HERE/lib.sh"
|
||||
|
||||
REG="$HOME/.claude/plugins/installed_plugins.json"
|
||||
MK="$HOME/.claude/plugins/known_marketplaces.json"
|
||||
|
||||
# 從檔案取 JSON 字串欄位。與 lib.sh 的 json_str 同一種 naive 解析,只是來源是檔案:
|
||||
# 先把換行換成空白、再以逗號斷行,這樣每行最多一個欄位,取值不會被貪婪比對吃掉。
|
||||
file_json_str() { # $1=檔案 $2=欄位名
|
||||
[ -f "$1" ] || return 0
|
||||
tr '\n' ' ' < "$1" | tr ',' '\n' \
|
||||
| sed -n "s/.*\"$2\"[[:space:]]*:[[:space:]]*\"\([^\"]*\)\".*/\1/p" | head -n1
|
||||
}
|
||||
|
||||
# 本機實際載入版本:先取該 plugin 的 installPath,再讀那個目錄下的 plugin.json。
|
||||
# 只認 installPath 底下那份檔案。註冊在 installed_plugins.json 的 version 欄位不當備援:
|
||||
# 註冊值可能比實際載入的版本新,拿它來比對會放過真正被載入的舊版,護欄形同虛設。
|
||||
# 讀不到那份檔案就當「查不到本機載入版本」,由呼叫端安靜放行。
|
||||
local_version() { # $1=domain
|
||||
[ -f "$REG" ] && [ -r "$REG" ] || return 0
|
||||
_seg=$(tr -d '\n' < "$REG" \
|
||||
| sed -n "s/.*\"jsc-$1@jsc\"[[:space:]]*:[[:space:]]*\[\([^]]*\)\].*/\1/p")
|
||||
[ -n "$_seg" ] || return 0
|
||||
_path=$(printf '%s' "$_seg" | tr ',' '\n' \
|
||||
| sed -n 's/.*"installPath"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)
|
||||
[ -n "$_path" ] || return 0
|
||||
file_json_str "$_path/plugin.json" version
|
||||
}
|
||||
|
||||
# 遠端站台與 owner:從已註冊的 jsc marketplace 來源推導,其次 GITEA_HOST。
|
||||
# 印出「{host} {owner}」;推導不出來時 host 為空字串。
|
||||
remote_host_owner() {
|
||||
_url=""
|
||||
if [ -f "$MK" ]; then
|
||||
_url=$(tr -d '\n' < "$MK" | sed -n 's/.*"jsc"[[:space:]]*:[[:space:]]*{//p' \
|
||||
| tr ',' '\n' \
|
||||
| sed -n 's/.*"url"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)
|
||||
fi
|
||||
_h=$(printf '%s' "$_url" | sed -n 's#^\(https\{0,1\}://[^/]*\)/.*#\1#p')
|
||||
_o=$(printf '%s' "$_url" | sed -n 's#^https\{0,1\}://[^/]*/\([^/]*\)/.*#\1#p')
|
||||
if [ -z "$_h" ] || [ -z "$_o" ]; then
|
||||
_h="${GITEA_HOST:-}"; _o="${JSC_GITEA_OWNER:-plugins}"
|
||||
fi
|
||||
printf '%s %s' "$_h" "$_o"
|
||||
}
|
||||
|
||||
# 遠端發佈版本:一律經由 jsc-gitea 的 gitea.sh(技能準則),它會帶 GITEA_TOKEN,
|
||||
# 並在缺 token 或 401/403 時退回 tea 的登入金鑰,私有存取庫才讀得到。
|
||||
# 不指定 ref:Gitea 的 raw 端點預設就取該存取庫的預設分支,比在這裡寫死分支名準。
|
||||
# 查不到就回傳空字串,由呼叫端放行。
|
||||
remote_version() { # $1=domain $2=host $3=owner
|
||||
_gsh=$(jsc_gitea_sh) || return 0
|
||||
_v=""
|
||||
for _try in 1 2; do # 暫時性網路失敗不該誤判成版本問題,失敗重試一次
|
||||
_body=$(GITEA_HOST="$2" sh "$_gsh" api GET "/repos/$3/$1/raw/plugin.json" 2>/dev/null) \
|
||||
&& _v=$(printf '%s' "$_body" | tr '\n' ' ' | tr ',' '\n' \
|
||||
| sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)
|
||||
[ -n "$_v" ] && break
|
||||
sleep 1
|
||||
done
|
||||
printf '%s' "$_v"
|
||||
}
|
||||
|
||||
TTL="${JSC_VERSION_TTL:-600}"
|
||||
cache_dir="$JSC_HOME/version-cache"
|
||||
|
||||
# 帶快取的遠端版本查詢。hook 與 report 共用同一份快取與同一個 TTL:
|
||||
# report 每個 domain 各打一次網路(還帶重試),/jsc-cli:deploy 一跑就是全部 domain,
|
||||
# 不共用快取等於每次部署都付一輪網路成本。
|
||||
cached_remote_version() { # $1=domain $2=host $3=owner
|
||||
_cache="$cache_dir/$1"
|
||||
_now=$(now_epoch)
|
||||
if [ -f "$_cache" ]; then
|
||||
_cv=$(cut -d' ' -f1 "$_cache" 2>/dev/null)
|
||||
_ca=$(cut -d' ' -f2 "$_cache" 2>/dev/null)
|
||||
if [ -n "$_cv" ] && [ -n "$_ca" ] && [ $((_now - _ca)) -lt "$TTL" ]; then
|
||||
printf '%s' "$_cv"; return 0
|
||||
fi
|
||||
fi
|
||||
_rv=$(remote_version "$1" "$2" "$3")
|
||||
if [ -n "$_rv" ]; then
|
||||
mkdir -p "$cache_dir" 2>/dev/null || true
|
||||
printf '%s %s\n' "$_rv" "$_now" > "$_cache" 2>/dev/null || true
|
||||
fi
|
||||
printf '%s' "$_rv"
|
||||
}
|
||||
|
||||
# 語意化比較:印出 -1($1 落後)、0(相等)、1($1 超前)
|
||||
ver_cmp() { # $1=版本 A $2=版本 B
|
||||
awk -v a="$1" -v b="$2" '
|
||||
BEGIN {
|
||||
n = split(a, x, "."); m = split(b, y, ".")
|
||||
for (i = 1; i <= 3; i++) {
|
||||
xi = (i <= n ? x[i] + 0 : 0); yi = (i <= m ? y[i] + 0 : 0)
|
||||
if (xi < yi) { print -1; exit }
|
||||
if (xi > yi) { print 1; exit }
|
||||
}
|
||||
print 0
|
||||
}'
|
||||
}
|
||||
|
||||
# ── report:一次比對所有已安裝的 jsc plugin(非 hook 模式,不讀 stdin)
|
||||
if [ "${1:-}" = "report" ]; then
|
||||
python3 - <<'PY'
|
||||
import json, os, re, subprocess, sys
|
||||
|
||||
home = os.path.expanduser("~")
|
||||
try:
|
||||
reg = json.load(open(f"{home}/.claude/plugins/installed_plugins.json")).get("plugins", {})
|
||||
except Exception:
|
||||
print("behind\t0"); sys.exit(0)
|
||||
|
||||
host = owner = ""
|
||||
try:
|
||||
mk = json.load(open(f"{home}/.claude/plugins/known_marketplaces.json"))
|
||||
m = re.match(r"(https?://[^/]+)/([^/]+)/[^/]+?(?:\.git)?/?$",
|
||||
(mk.get("jsc", {}).get("source", {}) or {}).get("url", ""))
|
||||
if m:
|
||||
host, owner = m.group(1), m.group(2)
|
||||
except Exception:
|
||||
pass
|
||||
if not host:
|
||||
host = os.environ.get("GITEA_HOST", "")
|
||||
owner = os.environ.get("JSC_GITEA_OWNER", "plugins")
|
||||
|
||||
|
||||
def ver(v):
|
||||
parts = ((v or "").split(".") + ["0", "0", "0"])[:3]
|
||||
return tuple(int(p) if p.isdigit() else 0 for p in parts)
|
||||
|
||||
|
||||
behind = 0
|
||||
for key in sorted(reg):
|
||||
m = re.match(r"^jsc-([^@]+)@", key)
|
||||
if not m:
|
||||
continue
|
||||
domain = m.group(1)
|
||||
# 本機版本一律以 installPath 底下那份 plugin.json 為準(實際載入版本),
|
||||
# 讀不到才退回註冊欄位。
|
||||
local = ""
|
||||
for e in reg[key]:
|
||||
try:
|
||||
local = json.load(open(os.path.join(e.get("installPath", ""), "plugin.json"))).get("version", "")
|
||||
break
|
||||
except Exception:
|
||||
local = e.get("version", "")
|
||||
remote = ""
|
||||
if host:
|
||||
url = f"{host}/{owner}/{domain}/raw/branch/master/plugin.json"
|
||||
for _ in range(2): # 暫時性網路失敗不該誤判成版本問題,失敗重試一次
|
||||
try:
|
||||
out = subprocess.run(["curl", "-sS", "--max-time", "10", url],
|
||||
capture_output=True, text=True, timeout=20).stdout
|
||||
mm = re.search(r'"version"\s*:\s*"([^"]+)"', out)
|
||||
if mm:
|
||||
remote = mm.group(1)
|
||||
break
|
||||
except Exception:
|
||||
pass
|
||||
if not remote:
|
||||
state = "查詢失敗"
|
||||
elif ver(local) < ver(remote):
|
||||
state = "落後"; behind += 1
|
||||
elif ver(local) > ver(remote):
|
||||
state = "超前"
|
||||
else:
|
||||
state = "最新"
|
||||
print(f"{domain}\t{local or '?'}\t{remote or '?'}\t{state}")
|
||||
print(f"behind\t{behind}")
|
||||
PY
|
||||
# 註冊檔不存在或讀不到就明講。這裡不能只印 behind 0:呼叫端會把它讀成「都是最新」,
|
||||
# 於是把「這台機器無法做版本檢查」誤報成「不用更新」。
|
||||
# 舊版用 `tr -d '\n' < "$REG" 2>/dev/null`,那個 2>/dev/null 只蓋住 tr 的 stderr,
|
||||
# 蓋不住 shell 開檔失敗的訊息,所以沒裝 Claude 的機器會先漏一行 cannot open。
|
||||
if [ ! -f "$REG" ] || [ ! -r "$REG" ]; then
|
||||
printf 'noregistry\t%s\n' "$REG"
|
||||
printf 'behind\t0\n'
|
||||
exit 0
|
||||
fi
|
||||
ho=$(remote_host_owner)
|
||||
r_host=$(printf '%s' "$ho" | cut -d' ' -f1)
|
||||
r_owner=$(printf '%s' "$ho" | cut -d' ' -f2)
|
||||
behind=0
|
||||
domains=$(tr -d '\n' < "$REG" | tr ',' '\n' \
|
||||
| sed -n 's/.*"jsc-\([a-z0-9][a-z0-9-]*\)@[^"]*"[[:space:]]*:.*/\1/p' | sort -u)
|
||||
for d in $domains; do
|
||||
lv=$(local_version "$d")
|
||||
rv=""
|
||||
[ -n "$r_host" ] && rv=$(cached_remote_version "$d" "$r_host" "$r_owner")
|
||||
if [ -z "$rv" ]; then
|
||||
st="查詢失敗"
|
||||
else
|
||||
case "$(ver_cmp "$lv" "$rv")" in
|
||||
-1) st="落後"; behind=$((behind + 1)) ;;
|
||||
1) st="超前" ;;
|
||||
*) st="最新" ;;
|
||||
esac
|
||||
fi
|
||||
printf '%s\t%s\t%s\t%s\n' "$d" "${lv:-?}" "${rv:-?}" "$st"
|
||||
done
|
||||
printf 'behind\t%s\n' "$behind"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
@@ -107,11 +178,13 @@ read_stdin
|
||||
|
||||
[ "${JSC_VERSION_GUARD:-}" = "off" ] && exit 0
|
||||
|
||||
# 只管 Skill 工具
|
||||
tool=$(json_str tool_name)
|
||||
# 輸入相容:stdin JSON(Claude 格式)與環境變數(其他四支 CLI 接線時設定)都要收。
|
||||
# 只讀 stdin 的話,用環境變數餵資料的 CLI 一律拿到空值,檢查會整支靜靜放行。
|
||||
# 兩者都缺才是真的沒資料,那時照舊安靜降級 exit 0。
|
||||
tool="${JSC_TOOL_NAME:-${TOOL_NAME:-$(json_str tool_name)}}"
|
||||
[ -z "$tool" ] || [ "$tool" = "Skill" ] || exit 0
|
||||
|
||||
skill=$(json_str skill)
|
||||
skill="${JSC_SKILL:-${SKILL:-$(json_str skill)}}"
|
||||
[ -n "$skill" ] || exit 0
|
||||
|
||||
# 只管本技能組(jsc-{domain}:{name})
|
||||
@@ -129,95 +202,44 @@ case "$skill" in
|
||||
jsc-cli:deploy|jsc-hooks:hooks-install|jsc-cli:models|jsc-meta:*) exit 0 ;;
|
||||
esac
|
||||
|
||||
TTL="${JSC_VERSION_TTL:-600}"
|
||||
cache_dir="$JSC_HOME/version-cache"
|
||||
mkdir -p "$cache_dir" 2>/dev/null || true
|
||||
# 更新指令依實際 CLI 給。印別的 CLI 的指令等於沒給指令,使用者照著打只會失敗。
|
||||
update_cmd() { # $1=domain
|
||||
case "$(cli_name)" in
|
||||
claude)
|
||||
printf 'claude plugin marketplace update jsc && claude plugin update jsc-%s@jsc' "$1" ;;
|
||||
codex)
|
||||
printf 'codex plugin marketplace upgrade jsc' ;;
|
||||
copilot)
|
||||
printf 'copilot plugin marketplace update jsc && copilot plugin update jsc-%s@jsc' "$1" ;;
|
||||
antigravity)
|
||||
printf 'git -C ~/plugins/%s pull && agy plugin uninstall jsc-%s && agy plugin install ~/plugins/%s' "$1" "$1" "$1" ;;
|
||||
kiro)
|
||||
printf 'kiro-cli plugin marketplace update jsc && kiro-cli plugin update jsc-%s@jsc' "$1" ;;
|
||||
*)
|
||||
printf '用你的 CLI 的 plugin 更新指令更新 jsc-%s@jsc' "$1" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
deny() { # $1=訊息
|
||||
printf '[jsc][版本檢查][ERR]:%s\n' "$1" >&2
|
||||
printf '更新指令:claude plugin marketplace update jsc && claude plugin update jsc-%s@jsc\n' "$domain" >&2
|
||||
printf '更新指令:%s\n' "$(update_cmd "$domain")" >&2
|
||||
printf '更新整組:/jsc-cli:deploy | 確定要略過檢查:JSC_VERSION_GUARD=off\n' >&2
|
||||
exit 2
|
||||
}
|
||||
|
||||
# 本機實際載入版本:從 installPath 的 plugin.json 讀,不用註冊欄位
|
||||
local_ver=$(python3 - "$domain" <<'PY' 2>/dev/null
|
||||
import json, os, sys
|
||||
domain = sys.argv[1]
|
||||
try:
|
||||
reg = json.load(open(os.path.expanduser("~/.claude/plugins/installed_plugins.json")))
|
||||
except Exception:
|
||||
sys.exit(0)
|
||||
entries = reg.get("plugins", {}).get(f"jsc-{domain}@jsc") or []
|
||||
for e in entries:
|
||||
p = os.path.join(e.get("installPath", ""), "plugin.json")
|
||||
try:
|
||||
print(json.load(open(p)).get("version", "")); sys.exit(0)
|
||||
except Exception:
|
||||
continue
|
||||
# 讀不到實際檔案時退回註冊版本,並在後面標記為次要來源
|
||||
if entries and entries[0].get("version"):
|
||||
print(entries[0]["version"])
|
||||
PY
|
||||
)
|
||||
[ -n "$local_ver" ] || deny "讀不到本機 jsc-$domain 的實際載入版本(installed_plugins.json 或該版目錄的 plugin.json 不可用)"
|
||||
# 讀不到本機實際載入版本就放行:沒有版本證據時擋下等於停掉每一次技能呼叫
|
||||
local_ver=$(local_version "$domain")
|
||||
[ -n "$local_ver" ] || exit 0
|
||||
|
||||
# 遠端站台與 owner:從已註冊的 jsc marketplace 來源推導,其次 GITEA_HOST
|
||||
remote_src=$(python3 - <<'PY' 2>/dev/null
|
||||
import json, os, re
|
||||
try:
|
||||
d = json.load(open(os.path.expanduser("~/.claude/plugins/known_marketplaces.json")))
|
||||
except Exception:
|
||||
raise SystemExit
|
||||
url = (d.get("jsc", {}).get("source", {}) or {}).get("url", "")
|
||||
m = re.match(r"(https?://[^/]+)/([^/]+)/[^/]+?(?:\.git)?/?$", url)
|
||||
if m:
|
||||
print(m.group(1), m.group(2))
|
||||
PY
|
||||
)
|
||||
host=$(printf '%s' "$remote_src" | cut -d' ' -f1)
|
||||
owner=$(printf '%s' "$remote_src" | cut -d' ' -f2)
|
||||
if [ -z "$host" ] || [ -z "$owner" ]; then
|
||||
host="${GITEA_HOST:-}"; owner="${JSC_GITEA_OWNER:-plugins}"
|
||||
fi
|
||||
[ -n "$host" ] || deny "推導不出 Gitea 站台(known_marketplaces.json 無 jsc 來源,GITEA_HOST 也未設定)"
|
||||
ho=$(remote_host_owner)
|
||||
host=$(printf '%s' "$ho" | cut -d' ' -f1)
|
||||
owner=$(printf '%s' "$ho" | cut -d' ' -f2)
|
||||
[ -n "$host" ] || exit 0
|
||||
|
||||
# 快取
|
||||
cache="$cache_dir/$domain"
|
||||
now=$(now_epoch)
|
||||
remote_ver=""
|
||||
if [ -f "$cache" ]; then
|
||||
c_ver=$(cut -d' ' -f1 "$cache" 2>/dev/null)
|
||||
c_at=$(cut -d' ' -f2 "$cache" 2>/dev/null)
|
||||
if [ -n "$c_ver" ] && [ -n "$c_at" ] && [ $((now - c_at)) -lt "$TTL" ]; then
|
||||
remote_ver="$c_ver"
|
||||
fi
|
||||
fi
|
||||
# 遠端版本(走 hook 與 report 共用的快取與 TTL)
|
||||
remote_ver=$(cached_remote_version "$domain" "$host" "$owner")
|
||||
[ -n "$remote_ver" ] || exit 0
|
||||
|
||||
if [ -z "$remote_ver" ]; then
|
||||
url="$host/$owner/$domain/raw/branch/master/plugin.json"
|
||||
# 暫時性網路失敗不該誤判成版本問題,所以失敗重試一次再放棄
|
||||
for _try in 1 2; do
|
||||
body=$(curl -sS --max-time 10 "$url" 2>/dev/null) && \
|
||||
remote_ver=$(printf '%s' "$body" | sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)
|
||||
[ -n "$remote_ver" ] && break
|
||||
sleep 1
|
||||
done
|
||||
[ -n "$remote_ver" ] || deny "查不到 jsc-$domain 的遠端發佈版本($url)。無法確認本機是否為最新,依 fail-closed 規則擋下"
|
||||
printf '%s %s\n' "$remote_ver" "$now" > "$cache" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# 語意化比較:只擋「本機 < 遠端」
|
||||
cmp=$(awk -v a="$local_ver" -v b="$remote_ver" '
|
||||
BEGIN {
|
||||
n = split(a, x, "."); m = split(b, y, ".")
|
||||
for (i = 1; i <= 3; i++) {
|
||||
xi = (i <= n ? x[i] + 0 : 0); yi = (i <= m ? y[i] + 0 : 0)
|
||||
if (xi < yi) { print -1; exit }
|
||||
if (xi > yi) { print 1; exit }
|
||||
}
|
||||
print 0
|
||||
}')
|
||||
|
||||
[ "$cmp" = "-1" ] || exit 0
|
||||
# 只擋「本機 < 遠端」這一種情況
|
||||
[ "$(ver_cmp "$local_ver" "$remote_ver")" = "-1" ] || exit 0
|
||||
deny "jsc-$domain 本機版本 $local_ver 落後遠端發佈版本 $remote_ver,本次技能呼叫已擋下"
|
||||
|
||||
Reference in New Issue
Block a user