feat(review 發布): 將 AI 審查結果集中到同一筆 Pull Review
This commit is contained in:
+65
-1
@@ -1,6 +1,6 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { postComment, postPullReviewComment } from './gitea.js';
|
||||
import { createPullReview, postComment, postPullReviewComment } from './gitea.js';
|
||||
import { FINDINGS_PATH } from './config.js';
|
||||
import { ok, line, warn } from './log.js';
|
||||
|
||||
@@ -36,6 +36,70 @@ function inlineCommentBody(f) {
|
||||
return `**等級**:${levelText(f)}\n**審查員**:${f.role}\n**建議**:${f.suggestion}`;
|
||||
}
|
||||
|
||||
function reviewSection(title, findings) {
|
||||
if (findings.length === 0) return '';
|
||||
return `${title}\n\n${buildTable(findings)}`;
|
||||
}
|
||||
|
||||
function findingGroupLabel(f) {
|
||||
if (!f.is_new) return '舊有未解決問題';
|
||||
if (f.level === 'critical') return '新嚴重問題';
|
||||
return '新發現問題';
|
||||
}
|
||||
|
||||
export function buildReviewPayload(intro, findings, { forceBodyFindings = false } = {}) {
|
||||
const inlineComments = [];
|
||||
const bodyFindings = [];
|
||||
|
||||
for (const f of findings) {
|
||||
const loc = forceBodyFindings ? null : parseLocation(f.location);
|
||||
if (loc) {
|
||||
inlineComments.push({
|
||||
path: loc.file,
|
||||
body: `**分類**:${findingGroupLabel(f)}\n${inlineCommentBody(f)}`,
|
||||
new_position: loc.line,
|
||||
});
|
||||
} else {
|
||||
bodyFindings.push(f);
|
||||
}
|
||||
}
|
||||
|
||||
const bodyOld = bodyFindings.filter(f => !f.is_new);
|
||||
const bodyNewNonCritical = bodyFindings.filter(f => f.is_new && f.level !== 'critical');
|
||||
const bodyNewCritical = bodyFindings.filter(f => f.is_new && f.level === 'critical');
|
||||
|
||||
const sections = [
|
||||
intro,
|
||||
reviewSection(`## 📋 無法行內標註的舊有未解決問題(${bodyOld.length} 筆)`, bodyOld),
|
||||
reviewSection(`## 🔍 無法行內標註的新發現問題(${bodyNewNonCritical.length} 筆)`, bodyNewNonCritical),
|
||||
reviewSection(`## 🚨 無法行內標註的新嚴重問題(${bodyNewCritical.length} 筆)`, bodyNewCritical),
|
||||
].filter(Boolean);
|
||||
|
||||
if (inlineComments.length > 0) {
|
||||
sections.push(`## 💬 行內標註問題(${inlineComments.length} 筆)\n\n詳見本 review 底下的行內 comments。`);
|
||||
}
|
||||
|
||||
return {
|
||||
body: sections.join('\n\n'),
|
||||
comments: inlineComments,
|
||||
};
|
||||
}
|
||||
|
||||
export async function postFindingsReview(intro, findings, deps = {}) {
|
||||
const { postReview = createPullReview } = deps;
|
||||
const payload = buildReviewPayload(intro, findings);
|
||||
try {
|
||||
await postReview(payload);
|
||||
ok(`Review 發布成功 (body findings=${findings.length - payload.comments.length} inline=${payload.comments.length})`);
|
||||
} catch (e) {
|
||||
if (payload.comments.length === 0) throw e;
|
||||
warn(`Review 行內 comment 批次發布失敗,改將所有問題放入同一筆 Review body: ${e.message}`);
|
||||
const fallbackPayload = buildReviewPayload(intro, findings, { forceBodyFindings: true });
|
||||
await postReview(fallbackPayload);
|
||||
ok(`Review 發布成功 (body findings=${findings.length} inline=0)`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 寫入 findings.json。
|
||||
* 預設寫到 workspace;若提供 mirrorDir,則同步寫入另一份供 repo commit 使用。
|
||||
|
||||
+17
-7
@@ -119,20 +119,30 @@ export async function postComment(body) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 在 PR 指定檔案的指定行數發布行內 review comment(標註程式碼位置)。
|
||||
* 透過 Gitea 的 pull reviews API,以 new_position 對應新版檔案的行號。
|
||||
* 若該行不在 diff 範圍內,Gitea 會回傳錯誤,由呼叫端決定是否降級為一般 comment。
|
||||
* 建立一筆 Pull Review。body 會成為 review 主內容,comments 會成為同一筆 review 底下的行內 comments。
|
||||
*/
|
||||
export async function postPullReviewComment({ path: filePath, line, body }) {
|
||||
export async function createPullReview({ body, comments = [], event = 'COMMENT' }) {
|
||||
const resp = await axios.post(
|
||||
api(`/repos/${GITEA_REPOSITORY}/pulls/${PR_NUMBER}/reviews`),
|
||||
{
|
||||
commit_id: PR_HEAD_SHA || undefined,
|
||||
event: 'COMMENT',
|
||||
body: '',
|
||||
comments: [{ path: filePath, body, new_position: line }],
|
||||
event,
|
||||
body,
|
||||
comments,
|
||||
},
|
||||
{ headers: headers(GITEA_COMMENT_TOKEN || GITEA_TOKEN), timeout: 30000, httpsAgent },
|
||||
);
|
||||
return resp.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 在 PR 指定檔案的指定行數發布行內 review comment(標註程式碼位置)。
|
||||
* 透過 Gitea 的 pull reviews API,以 new_position 對應新版檔案的行號。
|
||||
* 若該行不在 diff 範圍內,Gitea 會回傳錯誤,由呼叫端決定是否降級為一般 comment。
|
||||
*/
|
||||
export async function postPullReviewComment({ path: filePath, line, body }) {
|
||||
return createPullReview({
|
||||
body: '',
|
||||
comments: [{ path: filePath, body, new_position: line }],
|
||||
});
|
||||
}
|
||||
|
||||
+6
-13
@@ -1,9 +1,9 @@
|
||||
import path from 'path';
|
||||
import { GITEA_REPOSITORY, PR_NUMBER, PR_HEAD_BRANCH, PR_BASE_BRANCH, getLLMConfig, FINDINGS_PATH, EXCLUSIONS_PATH } from './config.js';
|
||||
import { loadRoles, getRoleIntro } from './roles.js';
|
||||
import { getPRDiff, postComment, getCommitMessageBySha, getBotReviewOutcome, shouldSkipBotCommit } from './gitea.js';
|
||||
import { getPRDiff, getCommitMessageBySha, getBotReviewOutcome, shouldSkipBotCommit } from './gitea.js';
|
||||
import { analyzeWithRole, loadOldFindings, mergeFindings, sortByLevel, deduplicateWithAI, loadExclusions, applyExclusions, filterFalsePositivesWithAI } from './findings.js';
|
||||
import { saveFindings, postOldFindingsComment, postNewNonCriticalComment, postNewCriticalComments } from './comments.js';
|
||||
import { saveFindings, postFindingsReview } from './comments.js';
|
||||
import { cloneRepo, commitAndPush, getRepoState } from './git.js';
|
||||
import { validateJSONArrayFile, ensureJSONArrayFileExists } from './json.js';
|
||||
import { runPreflight } from './preflight.js';
|
||||
@@ -63,13 +63,8 @@ async function main() {
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
try {
|
||||
const intro = getRoleIntro(roles) + `\n\n> 🔍 服務:${provider} 模型:${model}`;
|
||||
await postComment(intro);
|
||||
ok('角色介紹 comment 發布成功');
|
||||
} catch (e) {
|
||||
warn(`comment 發布失敗(繼續執行): ${e.message}`);
|
||||
}
|
||||
const reviewIntro = getRoleIntro(roles) + `\n\n> 🔍 服務:${provider} 模型:${model}`;
|
||||
ok('角色介紹 Review 內容已準備');
|
||||
|
||||
step('Step2', 'Findings 產生');
|
||||
const results = await Promise.allSettled(roles.map(role => analyzeWithRole(role, diff)));
|
||||
@@ -111,12 +106,10 @@ async function main() {
|
||||
const reviewDir = repoDir || WORKSPACE;
|
||||
saveFindings(WORKSPACE, filtered, reviewDir);
|
||||
try {
|
||||
await postOldFindingsComment(filtered);
|
||||
await postNewNonCriticalComment(filtered);
|
||||
await postNewCriticalComments(filtered);
|
||||
await postFindingsReview(reviewIntro, filtered);
|
||||
ok('Step5 完成');
|
||||
} catch (e) {
|
||||
warn(`comment 發布失敗(繼續執行): ${e.message}`);
|
||||
warn(`Review 發布失敗(繼續執行): ${e.message}`);
|
||||
}
|
||||
|
||||
step('Step6', 'JSON 格式驗證');
|
||||
|
||||
Reference in New Issue
Block a user