fix(diagnostics): 處理 ai review findings #32
+15
-10
@@ -210,7 +210,7 @@ async function main() {
|
||||
};
|
||||
/**
|
||||
* 建問題模式:建立追蹤 issue(標題=PR 標題、本文=PR 描述+回溯 PR 的引言,連同挑好的標籤一次建立),
|
||||
* 並把 `issueBuffer` 內暫存的情境留言批次寫入 issue;設定閉包變數 `issue` 供後續留言直接發到 issue。
|
||||
* 並把 `issueBuffer` 內暫存的情境留言依流程順序寫入 issue;設定閉包變數 `issue` 供後續留言直接發到 issue。
|
||||
* 僅於「確定有保留問題」時呼叫一次。標籤於建立時一次帶入,省去「先建空標籤 issue 再補掛」的多餘 API 往返。
|
||||
*
|
||||
* @param {number[]} [labelIds] - 建立 issue 時要一併掛上的標籤 id 陣列(由 `review.selectLabels` 事先挑選);
|
||||
@@ -224,9 +224,9 @@ async function main() {
|
||||
labels: labelIds,
|
||||
});
|
||||
log('建問題', 'INF', `已建立追蹤 issue #${issue.number},寫入 ${issueBuffer.length} 則情境留言。`);
|
||||
await Promise.all(
|
||||
issueBuffer.map((body) => gitea.createCommentOnIssue(ctx, issue.number, body)),
|
||||
);
|
||||
for (const body of issueBuffer) {
|
||||
await gitea.createCommentOnIssue(ctx, issue.number, body);
|
||||
}
|
||||
issueBuffer.length = 0;
|
||||
};
|
||||
|
||||
@@ -302,7 +302,9 @@ async function main() {
|
||||
await queueOrPostComment(templates.rolesComment({ title: '🛡️ 防守方登場', roles: defenders }));
|
||||
|
||||
// ── 步驟 8:防守方裁決 → 排除 → 排序 → 保存 findings ──────────────────
|
||||
const { kept, excluded } = await review.runDefenders({ tool, model: ctx.model, cwd, defenders, findings });
|
||||
const { kept, excluded } = findings.length === 0
|
||||
? { kept: [], excluded: [] }
|
||||
: await review.runDefenders({ tool, model: ctx.model, cwd, defenders, findings });
|
||||
review.sortFindings(kept);
|
||||
const relativePath = saveFindings({ cwd, ctx, tool, kept, excluded });
|
||||
|
||||
@@ -398,12 +400,15 @@ async function main() {
|
||||
}
|
||||
|
||||
// ── 收尾:commit 並 push(success=無嚴重問題、failure=有嚴重問題)───────
|
||||
// 一般模式:findings+exclusions.json;建問題模式:問題明細已在 issue 留言,只 commit exclusions.json。
|
||||
// 一般模式:findings+exclusions.json;建問題模式通常只 commit exclusions.json。
|
||||
// 若有嚴重問題,仍 commit findings 檔產生 [failure] 結果 commit,避免相依 API 不支援時 fail-open。
|
||||
const result = severe.length === 0 ? 'success' : 'failure';
|
||||
const filesToCommit = ctx.createIssue ? [] : [relativePath];
|
||||
if (exclusionsChanged) {
|
||||
filesToCommit.push(path.join('.gitea', 'ai-review', 'exclusions.json'));
|
||||
}
|
||||
const filesToCommit = review.resultFilesToCommit({
|
||||
createIssue: ctx.createIssue,
|
||||
severeCount: severe.length,
|
||||
relativePath,
|
||||
exclusionsChanged,
|
||||
});
|
||||
if (filesToCommit.length > 0) {
|
||||
commitFindings({ cwd, ctx, files: filesToCommit, result });
|
||||
} else {
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
'use strict';
|
||||
|
||||
// AI CLI 失敗診斷與機密遮罩工具:供 review 流程記錄安全、限長的一行錯誤摘要。
|
||||
|
||||
// 遮罩前先截去的輸入上限,避免對數 MB 失敗輸出跑整份 O(k*n) 正規掃描。
|
||||
const AGENT_DIAGNOSTIC_INPUT_LIMIT = 2_000;
|
||||
// 每段診斷片段(stderr/stdout)寫入日誌的字元上限。
|
||||
const AGENT_DIAGNOSTIC_OUTPUT_LIMIT = 500;
|
||||
|
||||
/**
|
||||
* 遮罩診斷文字中的機密與控制字元,避免寫進 CI log 時外洩。
|
||||
*
|
||||
* 處理順序:換行與控制字元一律壓成單一空白(避免注入假日誌行)→ 遮蔽
|
||||
* `Authorization` 標頭、`token=`/`token:` 型憑證、URL 內嵌帳密、以及常見長金鑰/
|
||||
* 長 hex/`ghp_` 等 token 樣式。屬「盡力遮罩」——無法窮舉所有機密格式,作為輸出
|
||||
* CLI 診斷片段前的防線使用(見 {@link agentFailureDetail})。
|
||||
*
|
||||
* @param {*} text - 待遮罩的原始文字(非字串會先以 `String()` 轉型)。
|
||||
* @returns {string} 已去控制字元並遮蔽常見機密樣式的單行文字。
|
||||
*/
|
||||
function redactSecrets(text) {
|
||||
return String(text ?? '')
|
||||
.replace(/[\r\n\t\v\f\x00-\x1f\x7f]+/g, ' ')
|
||||
.replace(/(authorization\s*[:=]\s*)(?:bearer\s+)?\S+/gi, '$1***')
|
||||
.replace(/((?:api[_-]?key|token|password|secret|bearer)\s*[:=]\s*)\S+/gi, '$1***')
|
||||
.replace(/(https?:\/\/)[^\s/:@]+:[^\s/@]+@/gi, '$1***:***@')
|
||||
.replace(/\bgh[pousr]_[A-Za-z0-9]{16,}\b/g, '***')
|
||||
.replace(/\b[A-Za-z0-9_-]{40,}\b/g, '***')
|
||||
.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* 從 `runAgent` 的失敗結果組出可診斷的一行摘要:退出碼/訊號為主,原始輸出預設隱藏。
|
||||
*
|
||||
* 安全考量:AI CLI 失敗時可能在 stderr/stdout 回顯提示內容、環境資訊、token、PII 或
|
||||
* 原始碼祕密,這些會被長期保存並供多人讀取的 CI log 收錄。因此本函式預設只輸出退出碼、
|
||||
* 訊號與逾時狀態;只有 `ACTIONS_STEP_DEBUG=true` 時才附上經 {@link redactSecrets}
|
||||
* 遮罩且去除控制字元的 stderr/stdout 片段。
|
||||
*
|
||||
* @param {{error: (Error & {code?: number|string, signal?: string, killed?: boolean})|null, stderr?: string, output?: string}} res
|
||||
* `runAgent` 的回傳物件。
|
||||
* @returns {string} 單行診斷摘要(各段以「|」分隔);無任何資訊時回傳固定字串。
|
||||
*/
|
||||
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}`);
|
||||
}
|
||||
// 失敗輸出可能含 token 或 PII,預設不寫入長期 CI log;debug 模式才輸出遮罩後片段。
|
||||
if (process.env.ACTIONS_STEP_DEBUG === 'true') {
|
||||
const stderr = redactSecrets(String((res && res.stderr) || '').slice(0, AGENT_DIAGNOSTIC_INPUT_LIMIT));
|
||||
if (stderr) parts.push(`stderr:${stderr.slice(0, AGENT_DIAGNOSTIC_OUTPUT_LIMIT)}`);
|
||||
const stdout = redactSecrets(String((res && res.output) || '').slice(0, AGENT_DIAGNOSTIC_INPUT_LIMIT));
|
||||
if (stdout) parts.push(`stdout:${stdout.slice(0, AGENT_DIAGNOSTIC_OUTPUT_LIMIT)}`);
|
||||
}
|
||||
if (parts.length === 0) {
|
||||
parts.push((err && err.message && redactSecrets(err.message)) || 'AI CLI 執行失敗(無診斷輸出)');
|
||||
}
|
||||
return parts.join('|');
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
AGENT_DIAGNOSTIC_INPUT_LIMIT,
|
||||
AGENT_DIAGNOSTIC_OUTPUT_LIMIT,
|
||||
redactSecrets,
|
||||
agentFailureDetail,
|
||||
};
|
||||
+24
-74
@@ -5,6 +5,7 @@ const path = require('path');
|
||||
|
||||
const { log, taipeiFromIso, taipeiNow } = require('./log');
|
||||
const { runAgent, extractJson } = require('./agents');
|
||||
const { agentFailureDetail } = require('./diagnostics');
|
||||
const templates = require('./templates');
|
||||
|
||||
// 審查流程核心:.reviewignore 過濾、diff 整理、攻擊方找問題、防守方裁決、排序分組與舊留言處理。
|
||||
@@ -13,76 +14,6 @@ const templates = require('./templates');
|
||||
const PER_FILE_DIFF_LIMIT = 16_000;
|
||||
const TOTAL_DIFF_LIMIT = 160_000;
|
||||
|
||||
// AI CLI 失敗診斷輸出政策常數(agentFailureDetail 使用):與上方送審上限並列於模組頂層,
|
||||
// 集中管理長度政策,避免藏在函式中段。
|
||||
// INPUT_LIMIT:遮罩前先截去的輸入上限——先截再跑 redactSecrets,避免對數 MB 失敗輸出跑整份
|
||||
// O(k×n) 正規掃描;2000 字已足以涵蓋跨界機密樣式。
|
||||
const INPUT_LIMIT = 2_000;
|
||||
// AGENT_DIAGNOSTIC_OUTPUT_LIMIT:每段診斷片段(stderr/stdout)寫入日誌的字元上限,兩段共用同一政策。
|
||||
const AGENT_DIAGNOSTIC_OUTPUT_LIMIT = 500;
|
||||
|
||||
/**
|
||||
* 遮罩單行診斷文字中的機密與控制字元,避免寫進 CI log 時外洩。
|
||||
*
|
||||
* 處理順序:換行與控制字元一律壓成單一空白(避免注入假日誌行)→ 遮蔽
|
||||
* `Authorization` 標頭、`token=`/`token:` 型憑證、URL 內嵌帳密、以及常見長金鑰/
|
||||
* 長 hex/`ghp_` 等 token 樣式。屬「盡力遮罩」——無法窮舉所有機密格式,作為輸出
|
||||
* CLI 診斷片段前的防線使用(見 {@link agentFailureDetail})。
|
||||
*
|
||||
* @param {*} text - 待遮罩的原始文字(非字串會先以 `String()` 轉型)。
|
||||
* @returns {string} 已去控制字元並遮蔽常見機密樣式的單行文字。
|
||||
* @remarks 本函式未匯出,僅供模組內部使用。
|
||||
*/
|
||||
function redactSecrets(text) {
|
||||
return String(text ?? '')
|
||||
.replace(/[\r\n\t\v\f\x00-\x1f\x7f]+/g, ' ')
|
||||
.replace(/(authorization\s*[:=]\s*)(?:bearer\s+)?\S+/gi, '$1***')
|
||||
.replace(/((?:api[_-]?key|token|password|secret|bearer)\s*[:=]\s*)\S+/gi, '$1***')
|
||||
.replace(/(https?:\/\/)[^\s/:@]+:[^\s/@]+@/gi, '$1***:***@')
|
||||
.replace(/\bgh[pousr]_[A-Za-z0-9]{16,}\b/g, '***')
|
||||
.replace(/\b[A-Za-z0-9_-]{40,}\b/g, '***')
|
||||
.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* 從 `runAgent` 的失敗結果組出可診斷的一行摘要:退出碼/訊號為主,原始輸出預設隱藏。
|
||||
*
|
||||
* 安全考量:AI CLI 失敗時可能在 stderr/stdout 回顯提示內容、環境資訊、token、PII 或
|
||||
* 原始碼祕密,這些會被長期保存並供多人讀取的 CI log 收錄。因此本函式預設只輸出退出碼、
|
||||
* 訊號與逾時狀態;只有 `ACTIONS_STEP_DEBUG=true` 時才附上經 {@link redactSecrets}
|
||||
* 遮罩且去除控制字元的 stderr/stdout 片段(各先截去過長輸入再取前 500 字)。
|
||||
*
|
||||
* @param {{error: (Error & {code?: number|string, signal?: string, killed?: boolean})|null, stderr?: string, output?: string}} res
|
||||
* `runAgent` 的回傳物件。
|
||||
* @returns {string} 單行診斷摘要(各段以「|」分隔);無任何資訊時回傳固定字串。
|
||||
* @remarks
|
||||
* 使用情境:{@link runAttackers}/{@link runDefenders}/{@link fillPurposes}/{@link selectLabels}
|
||||
* 判定 `!res.ok` 時,以本函式把失敗細節寫進 WRN log,讓 CI 記錄能看出 AI CLI 為何失敗;
|
||||
* 需要輸出片段輔助診斷時,於 workflow 設定 `ACTIONS_STEP_DEBUG=true` 再重跑。
|
||||
* 本函式未匯出,僅供模組內部使用。
|
||||
*/
|
||||
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}`);
|
||||
}
|
||||
// 失敗輸出可能含 token 或 PII,預設不寫入長期 CI log;debug 模式才輸出遮罩後片段。
|
||||
if (process.env.ACTIONS_STEP_DEBUG === 'true') {
|
||||
const stderr = redactSecrets(String((res && res.stderr) || '').slice(0, INPUT_LIMIT));
|
||||
if (stderr) parts.push(`stderr:${stderr.slice(0, AGENT_DIAGNOSTIC_OUTPUT_LIMIT)}`);
|
||||
const stdout = redactSecrets(String((res && res.output) || '').slice(0, INPUT_LIMIT));
|
||||
if (stdout) parts.push(`stdout:${stdout.slice(0, AGENT_DIAGNOSTIC_OUTPUT_LIMIT)}`);
|
||||
}
|
||||
if (parts.length === 0) {
|
||||
parts.push((err && err.message && redactSecrets(err.message)) || 'AI CLI 執行失敗(無診斷輸出)');
|
||||
}
|
||||
return parts.join('|');
|
||||
}
|
||||
|
||||
/**
|
||||
* 讀取工作目錄下的 `.reviewignore`,解析為忽略路徑前綴清單。
|
||||
*
|
||||
@@ -626,6 +557,28 @@ function appendExclusions({ cwd, excluded, prNumber }) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 依審查模式與結果決定收尾要提交的結果檔。
|
||||
*
|
||||
* 一般模式永遠提交本回合 findings 檔;建問題模式通常把問題明細留在 issue,不提交
|
||||
* findings。例外是有嚴重問題時仍提交 findings 檔,讓 `[failure]` 結果 commit 一定能產生,
|
||||
* 避免問題相依 API 不支援或設定失敗時 PR 缺少失敗檢查。
|
||||
*
|
||||
* @param {Object} params - 解構參數。
|
||||
* @param {boolean} params.createIssue - 是否啟用建問題模式。
|
||||
* @param {number} params.severeCount - 嚴重 finding 數量。
|
||||
* @param {string} params.relativePath - 本回合保存的 findings 檔 repo 相對路徑。
|
||||
* @param {boolean} params.exclusionsChanged - exclusions.json 是否有實際異動。
|
||||
* @returns {string[]} 應交給 `commitFindings` 的 repo 相對路徑清單。
|
||||
*/
|
||||
function resultFilesToCommit({ createIssue, severeCount, relativePath, exclusionsChanged }) {
|
||||
const files = createIssue ? (severeCount > 0 ? [relativePath] : []) : [relativePath];
|
||||
if (exclusionsChanged) {
|
||||
files.push(path.join('.gitea', 'ai-review', 'exclusions.json'));
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
/**
|
||||
* 就地排序 findings:依 嚴重→警告→建議、再依檔案路徑、再依起始行遞增。
|
||||
*
|
||||
@@ -926,13 +879,10 @@ module.exports = {
|
||||
runDefenders,
|
||||
sortFindings,
|
||||
appendExclusions,
|
||||
resultFilesToCommit,
|
||||
selectLabels,
|
||||
postSevereToIssue,
|
||||
postOthersToIssue,
|
||||
resolveOldComments,
|
||||
postSevereComments,
|
||||
__test: {
|
||||
agentFailureDetail,
|
||||
redactSecrets,
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user