feat(建問題模式): 審查留言改發到 issue、跳過舊留言處理並回貼 PR 連結

This commit is contained in:
Jeffery
2026-07-20 11:24:48 +08:00
parent af6b9a197f
commit 91fff79f22
4 changed files with 199 additions and 115 deletions
+99 -24
View File
@@ -117,9 +117,9 @@ function commitFindings({ cwd, ctx, files, result }) {
/**
* AI code review 主流程:依固定 10 步驟執行多角色審查,回傳 process exit code。
*
* 流程概要:
* 流程概要(步驟 2~10 描述一般模式;建問題模式差異見末段)
* 1. 快速回報 — 最新 commit 若為 ai-review-bot 的結果 commit[success]/[failure]),直接回報 0/1 不重審;
* 2. 將 PR 既有舊留言標記為解決(跳過本回合留言);
* 2. 將 PR 既有舊留言標記為解決(跳過本回合留言;建問題模式不執行此步);
* 3. 偵測 AI 工具(antigravitycodexclaude)並留言;
* 4. 讀 .reviewignore、整理 git diff 並留言(無可審查變更時:留言+保存空 findings,
* 一般模式 commit success、建問題模式略過 commit,回傳 0);
@@ -128,7 +128,10 @@ function commitFindings({ cwd, ctx, files, result }) {
* 並以 appendExclusions 把誤判/重複問題回寫 .gitea/ai-review/exclusions.json
* 9. 嚴重問題逐條掛在程式碼行上留言;
* 10. 警告+建議彙整為單一表格留言;
* 建問題模式(input: create-issue):保留問題另建 issuecreateIssueWithFindings)逐條留言明細;
* 建問題模式(input: create-issue):不執行步驟 2、不觸碰 PR 既有留言;步驟 3~10 的所有留言
* 改發到追蹤 issue(工具/diff/角色留言先暫存,確定有保留問題後才建立 issue 並一次寫入,
* 嚴重問題與警告+建議亦發到該 issue);無保留問題則不建 issue、僅在 PR 留審查通過提示;
* 收束時依保留問題補掛 issue 標籤,並在 PR 回貼 issue 連結形成雙向關聯;
* 收尾:組 filesToCommit —— 一般模式 commit findings 檔(+有變更的 exclusions.json)、
* 建問題模式只 commit exclusions.json、無檔案可 commit 時略過;
* commit 訊息帶結果標記(success=無嚴重問題、failure=有嚴重問題)。
@@ -167,28 +170,60 @@ async function main() {
return 1;
}
// 本回合發出的一般留言 id:步驟 2 標註過時時要跳過這些。
// 本回合(一般模式)發出的 PR 留言 idresolveOldComments 標註過時時要跳過這些。
const currentRunCommentIds = new Set();
// 建問題模式:issue 於「確定有保留問題」後才建立;在那之前的情境留言(工具/diff/角色)
// 先暫存於 issueBuffer,建立 issue 後一次寫入。
const issueBuffer = [];
let issue = null;
/**
* 建立本回合 PR 一般留言並記錄留言 id,供後續舊留言處理排除。
* 發布一則審查留言。依模式決定去向:
* - 一般模式:發到 PR,並記錄留言 id 供 `resolveOldComments` 排除。
* - 建問題模式:issue 已建立時發到 issue;尚未建立時先暫存到 `issueBuffer`。
*
* @param {string} body 要發布到 PR 的 Markdown 留言內容。
* @returns {Promise<Object>} Gitea API 建立的留言物件;至少預期包含 `id`。
* @param {string} body 要發布的 Markdown 留言內容。
* @returns {Promise<Object|null>} 一般模式、或建問題模式且 issue 已建立時回傳 Gitea 留言物件;
* 建問題模式尚未建立 issue 而先暫存時回傳 null。
* @remarks
* 使用情境:只在 `main()` 內部使用,處理工具資訊、diff 摘要、角色登場與
* 警告/建議彙整等一般留言。若 Gitea API 失敗,例外會往上拋出並由
* 主流程頂層 catch 收斂。
* 警告/建議彙整等留言。若 Gitea API 失敗,例外會往上拋出並由主流程頂層 catch 收斂。
*/
const postComment = async (body) => {
if (ctx.createIssue) {
if (issue) return gitea.createCommentOnIssue(ctx, issue.number, body);
issueBuffer.push(body);
return null;
}
const created = await gitea.createIssueComment(ctx, body);
currentRunCommentIds.add(created.id);
return created;
};
/**
* 建問題模式:建立追蹤 issue(標題=PR 標題、本文=PR 描述+回溯 PR 的引言,先不掛標籤),
* 並把 `issueBuffer` 內暫存的情境留言依序寫入 issue;設定閉包變數 `issue` 供後續留言直接發到 issue。
* 僅於「確定有保留問題」時呼叫一次。
*
* @returns {Promise<void>} 無回傳值;結果反映在閉包變數 `issue` 與 issue 留言。
*/
const ensureIssueCreated = async () => {
issue = await gitea.createIssue(ctx, {
title: ctx.prTitle || `AI Code ReviewPR #${ctx.prNumber}`,
body: templates.issueBody({ prNumber: ctx.prNumber, prBody: ctx.prBody }),
labels: [],
});
log('建問題', 'INF', `已建立追蹤 issue #${issue.number},寫入 ${issueBuffer.length} 則情境留言。`);
for (const body of issueBuffer) {
await gitea.createCommentOnIssue(ctx, issue.number, body);
}
issueBuffer.length = 0;
};
// ── 步驟 2:將 PR 既有留言標記為解決(本回合留言除外)───────────────────
// 早於偵測工具與所有本回合留言:先把上一回合的 bot 留言標為過時;
// 此時本回合尚未發出任何留言(currentRunCommentIds 為空),之後發出的留言自然不受影響
await review.resolveOldComments({ ctx, gitea, currentRunCommentIds });
// 僅一般模式執行;建問題模式不觸碰 PR 既有留言(審查內容改發到 issue)。
// 早於偵測工具與所有本回合留言:先把上一回合的 bot 留言標為過時(此時尚無本回合留言)
if (!ctx.createIssue) {
await review.resolveOldComments({ ctx, gitea, currentRunCommentIds });
}
// ── 步驟 3:偵測 AI agent 工具並留言 ──────────────────────────────────
const tool = agents.detectTool();
@@ -218,8 +253,13 @@ async function main() {
log('步驟4', 'INF', `變更檔案 ${allFiles.length} 個,套用 .reviewignore 後送審 ${files.length} 個(排除 ${ignoredCount} 個)。`);
if (files.length === 0) {
// 沒有可審查的變更:留言說明、保存空 findings、以 success 收場。
await postComment(templates.nothingToReviewComment(ignoredCount));
// 沒有可審查的變更:告知、保存空 findings、以 success 收場。
// 建問題模式無問題可追蹤 → 不建 issue,直接在 PR 告知(暫存的情境留言捨棄)。
if (ctx.createIssue) {
await gitea.createIssueComment(ctx, templates.nothingToReviewComment(ignoredCount));
} else {
await postComment(templates.nothingToReviewComment(ignoredCount));
}
const relativePath = saveFindings({ cwd, ctx, tool, kept: [], excluded: [] });
if (ctx.createIssue) {
// 建問題模式下 findings 不進版控,且 exclusions.json 無變更 → 沒東西可提交。
@@ -260,24 +300,59 @@ async function main() {
const others = kept.filter((finding) => finding.severity !== '嚴重');
log('步驟8', 'INF', `分組結果:嚴重 ${severe.length} 條、警告+建議 ${others.length} 條。`);
// ── 步驟 9:嚴重問題逐條掛在程式碼行上留言(開發者可回覆)──────────────
if (severe.length > 0) {
await review.postSevereComments({ ctx, gitea, severe, cwd });
// ── 建問題模式:確定有保留問題才建立 issue,並把暫存的情境留言一次寫入;
// 無保留問題則不建 issue,改在 PR 留一則審查通過提示。 ──────────────────
if (ctx.createIssue) {
if (kept.length > 0) {
await ensureIssueCreated();
} else {
log('建問題', 'INF', '沒有保留的問題,略過建立 issue,於 PR 留審查通過提示。');
await gitea.createIssueComment(ctx, templates.noFindingsComment());
}
}
// ── 步驟 10:警告+建議彙整為單一表格留言 ─────────────────────────────
// ── 步驟 9:嚴重問題留言(一般模式掛在 PR 程式碼行上;建問題模式發到 issue)
if (severe.length > 0) {
if (ctx.createIssue) {
await review.postSevereToIssue({ ctx, gitea, issueNumber: issue.number, severe });
} else {
await review.postSevereComments({ ctx, gitea, severe, cwd });
}
}
// ── 步驟 10:警告+建議彙整為單一表格留言(去向由 postComment 依模式決定)──
if (others.length > 0) {
await postComment(templates.othersComment(others));
log('步驟10', 'INF', `警告+建議表格留言已發布(${others.length} 條)。`);
}
// ── 建問題模式input: create-issue):另建 issue 逐條留言問題明細 ─────
if (ctx.createIssue) {
if (kept.length > 0) {
await review.createIssueWithFindings({ ctx, gitea, tool, model: ctx.model, cwd, findings: kept });
} else {
log('建問題', 'INF', '沒有保留的問題,略過建立 issue。');
// ── 建問題模式收束:依保留問題補掛 issue 標籤,並在 PR 回貼 issue 連結(雙向關聯)
if (ctx.createIssue && issue) {
try {
const labels = await gitea.listLabels(ctx);
const labelIds = await review.selectLabels({
tool,
model: ctx.model,
cwd,
labels,
prTitle: ctx.prTitle,
prBody: ctx.prBody,
findings: kept,
});
await gitea.addLabelsToIssue(ctx, issue.number, labelIds);
} catch (err) {
log('建問題', 'WRN', `補掛標籤失敗(${err.message}),issue 不掛標籤。`);
}
await gitea.createIssueComment(
ctx,
templates.issueLinkComment({
issueNumber: issue.number,
issueUrl: issue.html_url,
severeCount: severe.length,
otherCount: others.length,
}),
);
log('建問題', 'INF', `issue #${issue.number} 已寫入審查內容,並在 PR 回貼連結。`);
}
// ── 收尾:commit 並 pushsuccess=無嚴重問題、failure=有嚴重問題)───────