diff --git a/src/lib/review.js b/src/lib/review.js index 172f22c..6b45694 100644 --- a/src/lib/review.js +++ b/src/lib/review.js @@ -18,8 +18,8 @@ const TOTAL_DIFF_LIMIT = 160_000; * * 處理順序:換行與控制字元一律壓成單一空白(避免注入假日誌行)→ 遮蔽 * `Authorization` 標頭、`token=`/`token:` 型憑證、URL 內嵌帳密、以及常見長金鑰/ - * 長 hex/`ghp_` 等 token 樣式。屬「盡力遮罩」——無法窮舉所有機密格式,故僅在 - * 明確開啟除錯輸出時作為第二道防線使用(見 {@link agentFailureDetail})。 + * 長 hex/`ghp_` 等 token 樣式。屬「盡力遮罩」——無法窮舉所有機密格式,作為輸出 + * CLI 診斷片段前的防線使用(見 {@link agentFailureDetail})。 * * @param {*} text - 待遮罩的原始文字(非字串會先以 `String()` 轉型)。 * @returns {string} 已去控制字元並遮蔽常見機密樣式的單行文字。 @@ -40,10 +40,11 @@ function redactSecrets(text) { * 從 `runAgent` 的失敗結果組出可診斷的一行摘要:退出碼/訊號為主,原始輸出預設隱藏。 * * 安全考量:AI CLI 失敗時可能在 stderr/stdout 回顯提示內容、環境資訊、token、PII 或 - * 原始碼祕密,這些會被長期保存並供多人讀取的 CI log 收錄。故本函式**預設只輸出** - * exit code/signal/killed 等不含機密的分類資訊;**僅在明確開啟 Actions step debug** - * (環境變數 `ACTIONS_STEP_DEBUG=true`)時,才附上經 {@link redactSecrets} 遮罩且去除 - * 控制字元的 stderr/stdout 片段(各截前 500 字)作為診斷第二選擇。純函式、不拋例外。 + * 原始碼祕密,這些會被長期保存並供多人讀取的 CI log 收錄。權衡「可除錯性」後:本函式 + * 於失敗時**預設**附上經 {@link redactSecrets} 遮罩且去除控制字元的 **stderr** 片段 + * (先截去過長輸入再取前 500 字)——只印 exit code 幾乎無從判斷 CLI + * 為何失敗;**另在明確開啟 Actions step debug**(環境變數 `ACTIONS_STEP_DEBUG=true`)時, + * 再附上同樣遮罩的 **stdout** 片段作為第二診斷來源。純函式、不拋例外。 * * @param {{error: (Error & {code?: number|string, signal?: string, killed?: boolean})|null, stderr?: string, output?: string}} res * `runAgent` 的回傳物件。 @@ -63,20 +64,21 @@ function agentFailureDetail(res) { else if (err.code) parts.push(`code ${err.code}`); else if (err.signal) parts.push(`signal ${err.signal}`); } - // 預設不輸出 AI CLI 原始 stderr/stdout(可能含 token、PII 或原始碼祕密); - // 僅在明確開啟 Actions step debug 時,附上「已遮罩+去控制字元」的片段作為診斷。 + // 先截去過長輸入再遮罩,避免對數 MB 的失敗輸出跑整份 O(k×n) 正規掃描; + // 2000 字上限已足以涵蓋跨界機密樣式,最終仍截為 500 字。 + const DEBUG_INPUT_LIMIT = 2_000; + // 預設即附上「經 redactSecrets 遮罩+去控制字元+限長」的 stderr 片段——CLI 失敗時若只印 + // exit code 幾乎無從除錯(見 test-claude 秒失敗案例);遮罩為盡力防線,仍以此權衡可除錯性。 + const stderr = redactSecrets(String((res && res.stderr) || '').slice(0, DEBUG_INPUT_LIMIT)); + if (stderr) parts.push(`stderr:${stderr.slice(0, 500)}`); + // 明確開啟 Actions step debug 時,再附上 stdout 片段作為第二診斷來源。 const verbose = String(process.env.ACTIONS_STEP_DEBUG || '').trim().toLowerCase() === 'true'; if (verbose) { - // 先截去過長輸入再遮罩,避免對數 MB 的失敗輸出跑整份 O(k×n) 正規掃描; - // 2000 字上限已足以涵蓋跨界機密樣式,最終仍截為 500 字。 - const DEBUG_INPUT_LIMIT = 2_000; - const stderr = redactSecrets(String((res && res.stderr) || '').slice(0, DEBUG_INPUT_LIMIT)); - if (stderr) parts.push(`stderr:${stderr.slice(0, 500)}`); const stdout = redactSecrets(String((res && res.output) || '').slice(0, DEBUG_INPUT_LIMIT)); if (stdout) parts.push(`stdout:${stdout.slice(0, 500)}`); } if (parts.length === 0) { - parts.push((err && err.message && redactSecrets(err.message)) || 'AI CLI 執行失敗(詳細輸出已隱藏)'); + parts.push((err && err.message && redactSecrets(err.message)) || 'AI CLI 執行失敗(無診斷輸出)'); } return parts.join('|'); }