fix(AI 失敗診斷): CLI 失敗預設輸出經遮罩的 stderr 片段以利除錯
node-actions/template: CI / BUILD (pull_request) Successful in 4s
CI / TEST (Claude) (pull_request) Successful in 23s
CI / TEST (Antigravity) (pull_request) Failing after 40s
CI / TEST (Codex) (pull_request) Failing after 2m39s

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jeffery
2026-07-20 16:17:59 +08:00
co-authored by Claude Opus 4.8
parent 7b5c7c3e4f
commit 71b93a885a
+16 -14
View File
@@ -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 codesignalkilled 等不含機密的分類資訊;**僅在明確開啟 Actions step debug**
* 環境變數 `ACTIONS_STEP_DEBUG=true`)時,才附上經 {@link redactSecrets} 遮罩且去除
* 控制字元的 stderrstdout 片段(各截前 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('');
}