fix(version-guard): 遠端版本查詢加上重試,避免暫時性網路失敗誤判 fail-closed #12
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "jsc-shared",
|
"name": "jsc-shared",
|
||||||
"version": "0.2.3",
|
"version": "0.2.4",
|
||||||
"description": "JSC 跨 AI 助理共用規範 skills plugin(Claude Code / Codex / Antigravity / OpenCode / GitHub Copilot CLI),`skills/` 為唯一真實來源,並提供整組 plugin 的安裝/更新/移除管理(plugins-install 一次安裝或更新 jsc-code/jsc-doc/jsc-persona/jsc-shared,plugins-uninstall 一次移除四個 JSC plugin)。安裝與更新一律以 Gitea 遠端 repo 的 README 與檔案為準,不依賴既有本機存取庫;所有 skills 以 SKILL.md 為共通標準;於 Claude Code 以 /jsc-shared: 前綴呼叫。",
|
"description": "JSC 跨 AI 助理共用規範 skills plugin(Claude Code / Codex / Antigravity / OpenCode / GitHub Copilot CLI),`skills/` 為唯一真實來源,並提供整組 plugin 的安裝/更新/移除管理(plugins-install 一次安裝或更新 jsc-code/jsc-doc/jsc-persona/jsc-shared,plugins-uninstall 一次移除四個 JSC plugin)。安裝與更新一律以 Gitea 遠端 repo 的 README 與檔案為準,不依賴既有本機存取庫;所有 skills 以 SKILL.md 為共通標準;於 Claude Code 以 /jsc-shared: 前綴呼叫。",
|
||||||
"skills": "./skills",
|
"skills": "./skills",
|
||||||
"author": {
|
"author": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "jsc-shared",
|
"name": "jsc-shared",
|
||||||
"version": "0.2.3",
|
"version": "0.2.4",
|
||||||
"description": "JSC 跨 AI 助理共用規範 skills plugin,`skills/` 為唯一真實來源,並提供整組 plugin 的安裝/更新/移除管理(plugins-install 一次安裝或更新 jsc-code/jsc-doc/jsc-persona/jsc-shared,plugins-uninstall 一次移除四個 JSC plugin)。安裝與更新一律以 Gitea 遠端 repo 的 README 與檔案為準,不依賴既有本機存取庫;所有 skills 以 SKILL.md 為共通標準。",
|
"description": "JSC 跨 AI 助理共用規範 skills plugin,`skills/` 為唯一真實來源,並提供整組 plugin 的安裝/更新/移除管理(plugins-install 一次安裝或更新 jsc-code/jsc-doc/jsc-persona/jsc-shared,plugins-uninstall 一次移除四個 JSC plugin)。安裝與更新一律以 Gitea 遠端 repo 的 README 與檔案為準,不依賴既有本機存取庫;所有 skills 以 SKILL.md 為共通標準。",
|
||||||
"skills": "./skills"
|
"skills": "./skills"
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "jsc-shared",
|
"name": "jsc-shared",
|
||||||
"version": "0.2.3",
|
"version": "0.2.4",
|
||||||
"description": "JSC 跨 AI 助理共用規範 skills plugin,`skills/` 為唯一真實來源,並提供整組 plugin 的安裝/更新/移除管理(plugins-install 一次安裝或更新 jsc-code/jsc-doc/jsc-persona/jsc-shared,plugins-uninstall 一次移除四個 JSC plugin)。安裝與更新一律以 Gitea 遠端 repo 的 README 與檔案為準,不依賴既有本機存取庫;所有 skills 以 SKILL.md 為共通標準;於 Antigravity 以 /jsc-shared: 前綴呼叫。",
|
"description": "JSC 跨 AI 助理共用規範 skills plugin,`skills/` 為唯一真實來源,並提供整組 plugin 的安裝/更新/移除管理(plugins-install 一次安裝或更新 jsc-code/jsc-doc/jsc-persona/jsc-shared,plugins-uninstall 一次移除四個 JSC plugin)。安裝與更新一律以 Gitea 遠端 repo 的 README 與檔案為準,不依賴既有本機存取庫;所有 skills 以 SKILL.md 為共通標準;於 Antigravity 以 /jsc-shared: 前綴呼叫。",
|
||||||
"skills": "./skills"
|
"skills": "./skills"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
* --plugin-root 本地 plugin 根目錄(含 plugin.json)。省略時使用 $CLAUDE_PLUGIN_ROOT;
|
* --plugin-root 本地 plugin 根目錄(含 plugin.json)。省略時使用 $CLAUDE_PLUGIN_ROOT;
|
||||||
* 兩者皆無則視為無法判定,fail-closed。
|
* 兩者皆無則視為無法判定,fail-closed。
|
||||||
* --host Gitea 主機,省略時依序取 $GITEA_HOST,再退回 gitea.jsc.idv.tw。
|
* --host Gitea 主機,省略時依序取 $GITEA_HOST,再退回 gitea.jsc.idv.tw。
|
||||||
* --timeout-ms 遠端查詢逾時毫秒數,預設 5000。
|
* --timeout-ms 遠端查詢單次逾時毫秒數,預設 3000(最多重試 3 次,見下方「重試」說明)。
|
||||||
*
|
*
|
||||||
* 兩種呼叫模式(自動偵測,不需額外旗標):
|
* 兩種呼叫模式(自動偵測,不需額外旗標):
|
||||||
* 1. hook 模式:stdin 餵進合法 JSON(Claude Code/Copilot 的 PreToolUse event)時,
|
* 1. hook 模式:stdin 餵進合法 JSON(Claude Code/Copilot 的 PreToolUse event)時,
|
||||||
@@ -22,12 +22,18 @@
|
|||||||
* 2. CLI/測試模式:stdin 沒有合法 JSON(互動終端機或空管線)時,用 exit code 表示結果
|
* 2. CLI/測試模式:stdin 沒有合法 JSON(互動終端機或空管線)時,用 exit code 表示結果
|
||||||
* (0=相符,非 0=阻擋),訊息走 stderr——供人工測試與端到端驗證使用。
|
* (0=相符,非 0=阻擋),訊息走 stderr——供人工測試與端到端驗證使用。
|
||||||
*
|
*
|
||||||
* 更新時間:2026/08/17 14:25:00
|
* 更新時間:2026/08/19 11:39:27
|
||||||
* 相依:Node.js 標準內建功能(fetch/fs/path),無外部套件;需 Node ≥ 20.12 並以
|
* 相依:Node.js 標準內建功能(fetch/fs/path),無外部套件;需 Node ≥ 20.12 並以
|
||||||
* `node --use-system-ca` 啟動,讓 fetch 走系統信任的 CA store(而非 Node 內建的
|
* `node --use-system-ca` 啟動,讓 fetch 走系統信任的 CA store(而非 Node 內建的
|
||||||
* Mozilla CA 清單)——在企業網路/代理攔截 TLS 的環境下,curl 等工具通常已信任
|
* Mozilla CA 清單)——在企業網路/代理攔截 TLS 的環境下,curl 等工具通常已信任
|
||||||
* 系統憑證,但 Node fetch 預設不會,若不加這個旗標會誤判成「查無法取得遠端版本」
|
* 系統憑證,但 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 端點匿名可讀),輸出仍套用
|
* 機密:不涉及 token(遠端 raw plugin.json 端點匿名可讀),輸出仍套用
|
||||||
* shared/scripts/lib/redact-patterns.json 的遮蔽規則以防萬一。
|
* shared/scripts/lib/redact-patterns.json 的遮蔽規則以防萬一。
|
||||||
* 退出碼(CLI/測試模式):0=版本相符;2=版本不符、查不到遠端、或本地版本無法判定(皆為 fail-closed)。
|
* 退出碼(CLI/測試模式):0=版本相符;2=版本不符、查不到遠端、或本地版本無法判定(皆為 fail-closed)。
|
||||||
@@ -50,8 +56,15 @@ function redact(text) {
|
|||||||
return out;
|
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) {
|
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++) {
|
for (let i = 0; i < argv.length; i++) {
|
||||||
const a = argv[i];
|
const a = argv[i];
|
||||||
if (a === '--plugin-root') args.pluginRoot = 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;
|
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 url = `https://${host}/api/v1/repos/plugins/${shortName}/raw/plugin.json?ref=master`;
|
||||||
const res = await fetch(url, { signal: AbortSignal.timeout(timeoutMs) });
|
const res = await fetch(url, { signal: AbortSignal.timeout(timeoutMs) });
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
@@ -91,6 +104,27 @@ async function fetchRemoteVersion(host, shortName, timeoutMs) {
|
|||||||
return body.version;
|
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) {
|
function blockMessage(pluginName, remoteVersion, loadedVersion) {
|
||||||
const remoteText = remoteVersion || '查無法取得遠端版本';
|
const remoteText = remoteVersion || '查無法取得遠端版本';
|
||||||
return (
|
return (
|
||||||
|
|||||||
Reference in New Issue
Block a user