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
+22 -78
View File
@@ -578,30 +578,6 @@ function sortFindings(findings) {
);
}
/**
* 建問題模式的就地排序:依檔案路徑、再依嚴重等級(嚴重→警告→建議)、再依起始行遞增。
*
* 與 {@link sortFindings}(嚴重度優先)不同,本排序以檔案路徑為第一鍵,
* 讓 issue 上逐條留言的問題「同檔集中」,便於開發者逐檔處理。
* 嚴重等級權重取自 `templates.SEVERITY_ORDER`;未知等級排最後。
* 注意:直接修改傳入陣列(in-place),無回傳值。
*
* @param {Array<{file: string, severity: string, startLine: number}>} findings - 要排序的 finding 陣列(通常為保留問題 `kept` 的複本)。
* @returns {void} 無回傳值;排序結果反映在傳入陣列本身。
* @remarks
* 使用情境:建問題模式(input: create-issue)下,{@link createIssueWithFindings}
* 先以 `[...findings]` 複製保留問題(不動原陣列的嚴重度排序),
* 再對複本呼叫本函式,依「檔案→嚴重度→行號」的順序逐條留言到新 issue。
*/
function sortFindingsForIssue(findings) {
findings.sort(
(a, b) =>
a.file.localeCompare(b.file) ||
(templates.SEVERITY_ORDER[a.severity] ?? 9) - (templates.SEVERITY_ORDER[b.severity] ?? 9) ||
a.startLine - b.startLine,
);
}
/**
* 建問題模式:以 AI 依 PR 標題/描述與問題列表摘要,
* 從存取庫可用標籤中挑選適合掛在追蹤 issue 上的標籤子集合。
@@ -620,9 +596,9 @@ function sortFindingsForIssue(findings) {
* @param {Array<Object>} params.findings - 保留的問題列表;每條取 severityfocusfile 與截斷 120 字的 problem 作為挑選依據。
* @returns {Promise<number[]>} 挑中的標籤 id 陣列(可用標籤的子集合);無適合標籤或任何失敗時為空陣列。
* @remarks
* 使用情境:建問題模式(input: create-issue)下,{@link createIssueWithFindings}
* 先呼叫 `gitea.listLabels` 取得可用標籤,再以本函式取得標籤 id 子集合,
* 傳給 `gitea.createIssue` 讓新 issue 自動掛上合適標籤
* 使用情境:建問題模式(input: create-issue)下,`main()`src/index.js
* 在建立追蹤 issue 後先呼叫 `gitea.listLabels` 取得可用標籤,再以本函式依保留問題
* 挑出標籤 id 子集合,交給 `gitea.addLabelsToIssue` 補掛到 issue
*/
async function selectLabels({ tool, model, cwd, labels, prTitle, prBody, findings }) {
if (labels.length === 0) return [];
@@ -670,61 +646,30 @@ ${JSON.stringify(brief)}
}
/**
* 建問題模式:把保留的審查問題建成存取庫的追蹤 issue 並逐條留言明細
* 建問題模式:把嚴重 findings 逐條以一般留言發到追蹤 issue
*
* 流程:AI 挑標籤(`listLabels` {@link selectLabels},失敗不掛標籤)
* → 建立 issue(標題=PR 標題、本文=PR 描述加追溯資訊;失敗記 ERR 並回傳 null 不阻斷主流程)
* → 複製 findings 依「檔案路徑→嚴重等級→起始行」排序({@link sortFindingsForIssue}
* → 逐條以 `templates.issueFindingComment` 留言到 issue
* issue 無法把留言掛在程式碼行上(沒有 diff 定位),故改以
* {@link templates.issueFindingComment} 在內文標明位置逐條發布——
* 等同一般模式 PR 步驟 9 的嚴重問題,改以 issue 留言呈現。
* findings 由呼叫端事先以 {@link sortFindings} 排序(嚴重度→檔案→行號),本函式不再排序
*
* @param {Object} params - 解構參數。
* @param {Object} params.ctx - 執行環境 context`loadContext()` 回傳);使用 `prNumber`、`prTitle`、`prBody` 及 Gitea API 認證欄位
* @param {Object} params.gitea - Gitea API 模組(src/lib/gitea.js);以參數注入便於測試替換,使用 `listLabels`、`createIssue`、`createCommentOnIssue`。
* @param {Object} params.tool - `detectTool()` 偵測到的 AI CLI 工具描述物件(挑標籤用)
* @param {string} params.model - 指定 AI 模型名稱;空字串=工具預設
* @param {string} params.cwd - agent 的工作目錄(repo 根目錄)
* @param {Array<Object>} params.findings - 要寫進 issue 的問題列表(通常為防守方裁決後保留的 `kept`);本函式以複本排序,不改動原陣列順序
* @returns {Promise<object|null>} 建立成功的 Gitea issue 物件(含 `number` 等欄位);建立 issue 失敗時為 null。
* @throws {Error} 逐條留言(`createCommentOnIssue`)失敗時未攔截、向上拋出;列標籤與建 issue 的失敗則已於函式內降級處理。
* @param {Object} params.ctx - 執行環境 context`loadContext()` 回傳); Gitea API 認證。
* @param {Object} params.gitea - Gitea API 模組(src/lib/gitea.js);以參數注入便於測試替換,使用 `createCommentOnIssue`。
* @param {number} params.issueNumber - 目標追蹤 issue 的編號
* @param {Array<Object>} params.severe - severity 為「嚴重」的 finding 列表(已排序;呼叫端保證非空)
* @returns {Promise<void>} 無回傳值;結果反映在 issue 留言與日誌
* @throws {Error} 逐條留言(`createCommentOnIssue`)失敗時未攔截、向上拋出,由主流程頂層 catch 收斂
* @remarks
* 使用情境:`main()`src/index.js在步驟 10 之後、收尾之前,
* 於 `ctx.createIssue` 為 true 且 `kept.length > 0` 時呼叫本函式
* 此模式下問題明細已保存在 issue 留言,收尾只 commit exclusions.json、findings 檔不進版控
* 使用情境:建問題模式(input: create-issue)下,`main()`src/index.js於防守方裁決後
* 建立追蹤 issue、寫入情境留言,再以本函式把嚴重問題逐條留言到該 issue
* 警告+建議則以 {@link templates.othersComment} 彙整成單一表格另發到同一 issue
*/
async function createIssueWithFindings({ ctx, gitea, tool, model, cwd, findings }) {
let labelIds = [];
try {
const labels = await gitea.listLabels(ctx);
labelIds = await selectLabels({
tool,
model,
cwd,
labels,
prTitle: ctx.prTitle,
prBody: ctx.prBody,
findings,
});
} catch (err) {
log('建問題', 'WRN', `取得存取庫標籤失敗(${err.message}),issue 不掛標籤。`);
async function postSevereToIssue({ ctx, gitea, issueNumber, severe }) {
for (const finding of severe) {
await gitea.createCommentOnIssue(ctx, issueNumber, templates.issueFindingComment(finding));
}
let issue;
try {
issue = await gitea.createIssue(ctx, {
title: ctx.prTitle || `AI Code ReviewPR #${ctx.prNumber}`,
body: templates.issueBody({ prNumber: ctx.prNumber, prBody: ctx.prBody }),
labels: labelIds,
});
} catch (err) {
log('建問題', 'ERR', `建立 issue 失敗:${err.message}`);
return null;
}
const sorted = [...findings];
sortFindingsForIssue(sorted);
for (const finding of sorted) {
await gitea.createCommentOnIssue(ctx, issue.number, templates.issueFindingComment(finding));
}
log('建問題', 'INF', `issue #${issue.number} 已建立並逐條留言 ${sorted.length} 條問題。`);
return issue;
log('步驟9', 'INF', `已將 ${severe.length} 條嚴重問題留言到 issue #${issueNumber}`);
}
/**
@@ -882,9 +827,8 @@ module.exports = {
runDefenders,
sortFindings,
appendExclusions,
sortFindingsForIssue,
selectLabels,
createIssueWithFindings,
postSevereToIssue,
resolveOldComments,
postSevereComments,
};