@@ -12,7 +12,7 @@
* --plugin-root 本地 plugin 根目錄(含 plugin.json)。省略時使用 $CLAUDE_PLUGIN_ROOT;
* 兩者皆無則視為無法判定,fail-closed。
* --host Gitea 主機,省略時依序取 $GITEA_HOST,再退回 gitea.jsc.idv.tw。
* --timeout-ms 遠端查詢逾時毫秒數,預設 5 000。
* --timeout-ms 遠端查詢單次 逾時毫秒數,預設 3 000(最多重試 3 次,見下方「重試」說明) 。
*
* 兩種呼叫模式(自動偵測,不需額外旗標):
* 1. hook 模式:stdin 餵進合法 JSON( Claude Code/ Copilot 的 PreToolUse event)時,
@@ -22,12 +22,18 @@
* 2. CLI/測試模式:stdin 沒有合法 JSON(互動終端機或空管線)時,用 exit code 表示結果
* (0=相符,非 0=阻擋),訊息走 stderr——供人工測試與端到端驗證使用。
*
* 更新時間:2026/08/17 14:25:00
* 更新時間:2026/08/19 11:39:27
* 相依:Node.js 標準內建功能(fetch/ fs/ path),無外部套件;需 Node ≥ 20.12 並以
* `node --use-system-ca` 啟動,讓 fetch 走系統信任的 CA store(而非 Node 內建的
* Mozilla CA 清單)——在企業網路/代理攔截 TLS 的環境下,curl 等工具通常已信任
* 系統憑證,但 Node fetch 預設不會,若不加這個旗標會誤判成「查無法取得遠端版本」
* 而非真正的版本落差;呼叫端(hooks.json)的 command 必須帶上此旗標。
* 而非真正的版本落差;呼叫端(hooks.json)的 command 必須帶上此旗標。TLS 憑證驗證
* 全程不跳過——此腳本的查詢結果直接決定要不要放行工具呼叫,跳過驗證等於讓中間人可
* 偽造回應誘導誤判,risk 遠大於單次連線失敗;遇到暫時性網路問題改用下方重試機制處理。
* 重試:遠端查詢對暫時性失敗(逾時/連線中斷/非 2xx/回應格式錯誤)最多重試到
* REMOTE_FETCH_MAX_ATTEMPTS 次,每次間隔 REMOTE_FETCH_RETRY_DELAY_MS,仍全部失敗才
* 視為「查無法取得遠端版本」fail-closed; --timeout-ms 預設從 5000 降到 3000,讓
* 「重試次數 × 單次逾時+重試間隔」的最長總耗時控制在 hooks.json 設定的 15 秒內。
* 機密:不涉及 token(遠端 raw plugin.json 端點匿名可讀),輸出仍套用
* shared/scripts/lib/redact-patterns.json 的遮蔽規則以防萬一。
* 退出碼(CLI/測試模式):0=版本相符;2=版本不符、查不到遠端、或本地版本無法判定(皆為 fail-closed)。
@@ -50,8 +56,15 @@ function redact(text) {
return out ;
}
const REMOTE _FETCH _MAX _ATTEMPTS = 3 ;
const REMOTE _FETCH _RETRY _DELAY _MS = 300 ;
function sleep ( ms ) {
return new Promise ( ( resolve ) => setTimeout ( resolve , ms ) ) ;
}
function parseArgs ( argv ) {
const args = { pluginRoot : null , host : null , timeoutMs : 5000 } ;
const args = { pluginRoot : null , host : null , timeoutMs : 3000 } ;
for ( let i = 0 ; i < argv . length ; i ++ ) {
const a = argv [ i ] ;
if ( a === '--plugin-root' ) args . pluginRoot = argv [ ++ i ] ;
@@ -78,7 +91,7 @@ function shortNameFromPluginName(name) {
return name . startsWith ( 'jsc-' ) ? name . slice ( 'jsc-' . length ) : name ;
}
async function fetchRemoteVersion ( host , shortName , timeoutMs ) {
async function fetchRemoteVersionOnce ( host , shortName , timeoutMs ) {
const url = ` https:// ${ host } /api/v1/repos/plugins/ ${ shortName } /raw/plugin.json?ref=master ` ;
const res = await fetch ( url , { signal : AbortSignal . timeout ( timeoutMs ) } ) ;
if ( ! res . ok ) {
@@ -91,6 +104,27 @@ async function fetchRemoteVersion(host, shortName, timeoutMs) {
return body . version ;
}
/** 對暫時性失敗(逾時/連線中斷/非 2xx/回應格式錯誤)重試,TLS 憑證驗證全程不跳過。 */
async function fetchRemoteVersion ( host , shortName , timeoutMs ) {
let lastErr ;
for ( let attempt = 1 ; attempt <= REMOTE _FETCH _MAX _ATTEMPTS ; attempt ++ ) {
try {
return await fetchRemoteVersionOnce ( host , shortName , timeoutMs ) ;
} catch ( err ) {
lastErr = err ;
if ( attempt < REMOTE _FETCH _MAX _ATTEMPTS ) {
log (
'WRN' ,
` 遠端版本查詢第 ${ attempt } 次失敗( ${ String ( err . message ) } ), ${ REMOTE _FETCH _RETRY _DELAY _MS } ms 後重試… ` ,
'版本檢查'
) ;
await sleep ( REMOTE _FETCH _RETRY _DELAY _MS ) ;
}
}
}
throw lastErr ;
}
function blockMessage ( pluginName , remoteVersion , loadedVersion ) {
const remoteText = remoteVersion || '查無法取得遠端版本' ;
return (