From 4a68743b66233520e22313235efd32897b67811a Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 20 Jul 2026 13:41:41 +0800 Subject: [PATCH] =?UTF-8?q?chore(=E5=AF=A9=E6=9F=A5=E8=A8=98=E9=8C=84):=20?= =?UTF-8?q?AI=20=E5=A4=B1=E6=95=97=E6=99=82=E8=BC=B8=E5=87=BA=20exit=20cod?= =?UTF-8?q?e=20=E8=88=87=20stderr=EF=BC=8Fstdout=20=E4=BE=9B=E8=A8=BA?= =?UTF-8?q?=E6=96=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/review.js | 40 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/src/lib/review.js b/src/lib/review.js index 73302b5..e197a77 100644 --- a/src/lib/review.js +++ b/src/lib/review.js @@ -13,6 +13,38 @@ const templates = require('./templates'); const PER_FILE_DIFF_LIMIT = 16_000; const TOTAL_DIFF_LIMIT = 160_000; +/** + * 從 `runAgent` 的失敗結果組出可診斷的一行摘要:退出碼/訊號/stderr/stdout 片段。 + * + * AI CLI 失敗時常見 stderr 為空、真正原因印在 stdout(例如 CLI 用法錯誤、未認證或額度提示), + * 若只記 `error.message` 會看不出原因。本函式把 exit code、stderr、stdout 各截前 500 字併成一行, + * 供各失敗點的 WRN log 使用。純函式、不拋例外。 + * + * @param {{error: (Error & {code?: number|string, signal?: string, killed?: boolean})|null, stderr?: string, output?: string}} res + * `runAgent` 的回傳物件。 + * @returns {string} 單行診斷摘要(各段以「|」分隔);無任何資訊時回傳 error.message 或「未知錯誤」。 + * @remarks + * 使用情境:{@link runAttackers}/{@link runDefenders}/{@link fillPurposes}/{@link selectLabels} + * 判定 `!res.ok` 時,以本函式把失敗細節寫進 WRN log,讓 CI 記錄能看出 AI CLI 為何失敗。 + * 本函式未匯出,僅供模組內部使用。 + */ +function agentFailureDetail(res) { + const parts = []; + const err = res && res.error; + if (err) { + if (err.killed) parts.push('已逾時終止'); + if (typeof err.code === 'number') parts.push(`exit ${err.code}`); + else if (err.code) parts.push(`code ${err.code}`); + else if (err.signal) parts.push(`signal ${err.signal}`); + } + const stderr = String((res && res.stderr) || '').trim(); + if (stderr) parts.push(`stderr:${stderr.slice(0, 500)}`); + const stdout = String((res && res.output) || '').trim(); + if (stdout) parts.push(`stdout:${stdout.slice(0, 500)}`); + if (parts.length === 0) parts.push((err && err.message) || '未知錯誤'); + return parts.join('|'); +} + /** * 讀取工作目錄下的 `.reviewignore`,解析為忽略路徑前綴清單。 * @@ -140,7 +172,7 @@ ${sections} - 不得輸出個資(PII)。`; const res = await runAgent(tool, { model, prompt, cwd, timeoutMs: 300_000 }); if (!res.ok) { - log('步驟4', 'WRN', '檔案用途摘要產生失敗,以「—」代替。'); + log('步驟4', 'WRN', `檔案用途摘要產生失敗,以「—」代替:${agentFailureDetail(res)}。`); return; } const parsed = extractJson(res.output); @@ -279,7 +311,7 @@ async function runAttackers({ tool, model, cwd, attackers, diffRows }) { log('步驟6', 'INF', `攻擊方 ${role.meta.name} 開始分析。`); const res = await runAgent(tool, { model, prompt: buildAttackPrompt(role, diffRows), cwd }); if (!res.ok) { - log('步驟6', 'WRN', `攻擊方 ${role.meta.name} 執行失敗:${(res.error && res.error.message) || '未知錯誤'}。`); + log('步驟6', 'WRN', `攻擊方 ${role.meta.name} 執行失敗:${agentFailureDetail(res)}。`); return []; } const parsed = extractJson(res.output); @@ -459,7 +491,7 @@ async function runDefenders({ tool, model, cwd, defenders, findings }) { }); const verdicts = new Map(); if (!res.ok) { - log('步驟8', 'WRN', `防守方 ${role.meta.name} 執行失敗,該角色視為全部保留。`); + log('步驟8', 'WRN', `防守方 ${role.meta.name} 執行失敗,該角色視為全部保留:${agentFailureDetail(res)}。`); return { role: role.meta.name, verdicts }; } const parsed = extractJson(res.output); @@ -633,7 +665,7 @@ ${JSON.stringify(brief)} - 沒有適合的標籤時輸出 []。`; const res = await runAgent(tool, { model, prompt, cwd, timeoutMs: 300_000 }); if (!res.ok) { - log('建問題', 'WRN', '標籤挑選失敗,issue 不掛標籤。'); + log('建問題', 'WRN', `標籤挑選失敗,issue 不掛標籤:${agentFailureDetail(res)}。`); return []; } const parsed = extractJson(res.output);