refactor(ai-review): 重新命名留言閉包並抽出診斷輸出長度常數

- postComment → queueOrPostComment:涵蓋建問題模式下可能只暫存不立即發布的語義。
- ensureIssueCreated → createIssueAndFlushBufferedComments:明示建立 issue 並清空暫存留言的完整行為。
- review.js agentFailureDetail 的 500 字上限抽為具名常數 AGENT_DIAGNOSTIC_OUTPUT_LIMIT。
純內部命名與可讀性調整,無外部行為變更。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jeffery
2026-07-20 18:04:23 +08:00
co-authored by Claude Opus 4.8
parent 83dd56f37f
commit 0c108ab7f9
2 changed files with 14 additions and 12 deletions
+10 -10
View File
@@ -198,7 +198,7 @@ async function main() {
* 使用情境:只在 `main()` 內部使用,處理工具資訊、diff 摘要、角色登場與 * 使用情境:只在 `main()` 內部使用,處理工具資訊、diff 摘要、角色登場與
* 警告/建議彙整等留言。若 Gitea API 失敗,例外會往上拋出並由主流程頂層 catch 收斂。 * 警告/建議彙整等留言。若 Gitea API 失敗,例外會往上拋出並由主流程頂層 catch 收斂。
*/ */
const postComment = async (body) => { const queueOrPostComment = async (body) => {
if (ctx.createIssue) { if (ctx.createIssue) {
if (issue) return gitea.createCommentOnIssue(ctx, issue.number, body); if (issue) return gitea.createCommentOnIssue(ctx, issue.number, body);
issueBuffer.push(body); issueBuffer.push(body);
@@ -217,7 +217,7 @@ async function main() {
* 空陣列或省略時不掛任何標籤(`gitea.createIssue` 對空陣列不帶 labels 欄位)。 * 空陣列或省略時不掛任何標籤(`gitea.createIssue` 對空陣列不帶 labels 欄位)。
* @returns {Promise<void>} 無回傳值;結果反映在閉包變數 `issue` 與 issue 留言。 * @returns {Promise<void>} 無回傳值;結果反映在閉包變數 `issue` 與 issue 留言。
*/ */
const ensureIssueCreated = async (labelIds = []) => { const createIssueAndFlushBufferedComments = async (labelIds = []) => {
issue = await gitea.createIssue(ctx, { issue = await gitea.createIssue(ctx, {
title: ctx.prTitle || `AI Code ReviewPR #${ctx.prNumber}`, title: ctx.prTitle || `AI Code ReviewPR #${ctx.prNumber}`,
body: templates.issueBody({ prNumber: ctx.prNumber, prBody: ctx.prBody }), body: templates.issueBody({ prNumber: ctx.prNumber, prBody: ctx.prBody }),
@@ -245,7 +245,7 @@ async function main() {
} }
log('步驟3', 'INF', `選用工具:${tool.name}${tool.version})。`); log('步驟3', 'INF', `選用工具:${tool.name}${tool.version})。`);
const runLink = `${ctx.serverUrl}/${ctx.repository}/actions/runs/${ctx.runId}`; const runLink = `${ctx.serverUrl}/${ctx.repository}/actions/runs/${ctx.runId}`;
await postComment( await queueOrPostComment(
templates.toolComment({ templates.toolComment({
toolName: tool.name, toolName: tool.name,
version: tool.version, version: tool.version,
@@ -270,7 +270,7 @@ async function main() {
if (ctx.createIssue) { if (ctx.createIssue) {
log('步驟4', 'INF', '建問題模式且無可審查變更:靜默通過(不建 issue、PR 不留言)。'); log('步驟4', 'INF', '建問題模式且無可審查變更:靜默通過(不建 issue、PR 不留言)。');
} else { } else {
await postComment(templates.nothingToReviewComment(ignoredCount)); await queueOrPostComment(templates.nothingToReviewComment(ignoredCount));
// 已成功產生本回合結果留言(無可審查變更),此時才把舊留言標為過時(本回合留言已排除)。 // 已成功產生本回合結果留言(無可審查變更),此時才把舊留言標為過時(本回合留言已排除)。
await review.resolveOldComments({ ctx, gitea, currentRunCommentIds }); await review.resolveOldComments({ ctx, gitea, currentRunCommentIds });
} }
@@ -286,20 +286,20 @@ async function main() {
const diffRows = review.collectDiffRows({ cwd, files, base, gitrepo }); const diffRows = review.collectDiffRows({ cwd, files, base, gitrepo });
await review.fillPurposes({ tool, model: ctx.model, cwd, diffRows }); await review.fillPurposes({ tool, model: ctx.model, cwd, diffRows });
await postComment(templates.diffComment(diffRows, ignoredCount)); await queueOrPostComment(templates.diffComment(diffRows, ignoredCount));
// ── 步驟 5:攻擊方角色登場留言 ───────────────────────────────────────── // ── 步驟 5:攻擊方角色登場留言 ─────────────────────────────────────────
const roles = loadRoles(path.join(ctx.actionPath, 'src', 'prompts', 'roles')); const roles = loadRoles(path.join(ctx.actionPath, 'src', 'prompts', 'roles'));
const attackers = attackersOf(roles); const attackers = attackersOf(roles);
const defenders = defendersOf(roles); const defenders = defendersOf(roles);
log('步驟5', 'INF', `攻擊方 ${attackers.length} 位、防守方 ${defenders.length} 位。`); log('步驟5', 'INF', `攻擊方 ${attackers.length} 位、防守方 ${defenders.length} 位。`);
await postComment(templates.rolesComment({ title: '⚔️ 攻擊方登場', roles: attackers })); await queueOrPostComment(templates.rolesComment({ title: '⚔️ 攻擊方登場', roles: attackers }));
// ── 步驟 6:每個攻擊方一個 sub agent 並行分析,合併問題列表 ──────────── // ── 步驟 6:每個攻擊方一個 sub agent 並行分析,合併問題列表 ────────────
const findings = await review.runAttackers({ tool, model: ctx.model, cwd, attackers, diffRows }); const findings = await review.runAttackers({ tool, model: ctx.model, cwd, attackers, diffRows });
// ── 步驟 7:防守方角色登場留言 ───────────────────────────────────────── // ── 步驟 7:防守方角色登場留言 ─────────────────────────────────────────
await postComment(templates.rolesComment({ title: '🛡️ 防守方登場', roles: defenders })); await queueOrPostComment(templates.rolesComment({ title: '🛡️ 防守方登場', roles: defenders }));
// ── 步驟 8:防守方裁決 → 排除 → 排序 → 保存 findings ────────────────── // ── 步驟 8:防守方裁決 → 排除 → 排序 → 保存 findings ──────────────────
const { kept, excluded } = await review.runDefenders({ tool, model: ctx.model, cwd, defenders, findings }); const { kept, excluded } = await review.runDefenders({ tool, model: ctx.model, cwd, defenders, findings });
@@ -335,7 +335,7 @@ async function main() {
} catch (err) { } catch (err) {
log('建問題', 'WRN', `標籤挑選失敗(${err.message}),issue 不掛標籤。`); log('建問題', 'WRN', `標籤挑選失敗(${err.message}),issue 不掛標籤。`);
} }
await ensureIssueCreated(labelIds); await createIssueAndFlushBufferedComments(labelIds);
} else { } else {
// 無保留問題 → 不建 issue、PR 也不留言(靜默通過,暫存的情境留言捨棄)。 // 無保留問題 → 不建 issue、PR 也不留言(靜默通過,暫存的情境留言捨棄)。
log('建問題', 'INF', '沒有保留的問題:靜默通過(不建 issue、PR 不留言)。'); log('建問題', 'INF', '沒有保留的問題:靜默通過(不建 issue、PR 不留言)。');
@@ -365,13 +365,13 @@ async function main() {
if (ctx.createIssue) { if (ctx.createIssue) {
await review.postOthersToIssue({ ctx, gitea, issueNumber: issue.number, others }); await review.postOthersToIssue({ ctx, gitea, issueNumber: issue.number, others });
} else { } else {
await postComment(templates.othersComment(others)); await queueOrPostComment(templates.othersComment(others));
log('步驟10', 'INF', `警告+建議表格留言已發布(${others.length} 條)。`); log('步驟10', 'INF', `警告+建議表格留言已發布(${others.length} 條)。`);
} }
} }
// ── 建問題模式收束:在 PR 回貼 issue 連結(雙向關聯);僅在有嚴重問題時才讓 PR 相依於該 issue ─ // ── 建問題模式收束:在 PR 回貼 issue 連結(雙向關聯);僅在有嚴重問題時才讓 PR 相依於該 issue ─
// 標籤已於建立 issue 時一次帶入(見上方 selectLabels → ensureIssueCreated),此處不再補掛。 // 標籤已於建立 issue 時一次帶入(見上方 selectLabels → createIssueAndFlushBufferedComments),此處不再補掛。
if (ctx.createIssue && issue) { if (ctx.createIssue && issue) {
await gitea.createIssueComment( await gitea.createIssueComment(
ctx, ctx,
+4 -2
View File
@@ -66,13 +66,15 @@ function agentFailureDetail(res) {
// 先截去過長輸入再遮罩,避免對數 MB 的失敗輸出跑整份 O(k×n) 正規掃描; // 先截去過長輸入再遮罩,避免對數 MB 的失敗輸出跑整份 O(k×n) 正規掃描;
// 2000 字上限已足以涵蓋跨界機密樣式,最終仍截為 500 字。 // 2000 字上限已足以涵蓋跨界機密樣式,最終仍截為 500 字。
const INPUT_LIMIT = 2_000; const INPUT_LIMIT = 2_000;
// 每段診斷片段(stderr/stdout)寫入日誌的字元上限,兩段共用同一政策,抽為具名常數避免兩處各寫一個魔術數字。
const AGENT_DIAGNOSTIC_OUTPUT_LIMIT = 500;
// 預設即附上「經 redactSecrets 遮罩+去控制字元+限長」的 stderr 與 stdout 片段——CLI 失敗時 // 預設即附上「經 redactSecrets 遮罩+去控制字元+限長」的 stderr 與 stdout 片段——CLI 失敗時
// 只印 exit code 幾乎無從除錯(見 test-claude 秒失敗案例);且部分 CLI(如 claude-code 的 // 只印 exit code 幾乎無從除錯(見 test-claude 秒失敗案例);且部分 CLI(如 claude-code 的
// -p 模式)會把錯誤寫到 stdout 而非 stderr,故兩者都輸出。redactSecrets 為盡力防線。 // -p 模式)會把錯誤寫到 stdout 而非 stderr,故兩者都輸出。redactSecrets 為盡力防線。
const stderr = redactSecrets(String((res && res.stderr) || '').slice(0, INPUT_LIMIT)); const stderr = redactSecrets(String((res && res.stderr) || '').slice(0, INPUT_LIMIT));
if (stderr) parts.push(`stderr${stderr.slice(0, 500)}`); if (stderr) parts.push(`stderr${stderr.slice(0, AGENT_DIAGNOSTIC_OUTPUT_LIMIT)}`);
const stdout = redactSecrets(String((res && res.output) || '').slice(0, INPUT_LIMIT)); const stdout = redactSecrets(String((res && res.output) || '').slice(0, INPUT_LIMIT));
if (stdout) parts.push(`stdout${stdout.slice(0, 500)}`); if (stdout) parts.push(`stdout${stdout.slice(0, AGENT_DIAGNOSTIC_OUTPUT_LIMIT)}`);
if (parts.length === 0) { if (parts.length === 0) {
parts.push((err && err.message && redactSecrets(err.message)) || 'AI CLI 執行失敗(無診斷輸出)'); parts.push((err && err.message && redactSecrets(err.message)) || 'AI CLI 執行失敗(無診斷輸出)');
} }