調整 AI review 以單一 Review 發布統計與 comments #28
+62
-1
@@ -1,11 +1,12 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { postComment, postPullReviewComment } from './gitea.js';
|
||||
import { postComment, postPullReviewComment, postPullReview } from './gitea.js';
|
||||
import { FINDINGS_PATH } from './config.js';
|
||||
import { ok, line, warn } from './log.js';
|
||||
|
||||
const LEVEL_EMOJI = { critical: '🔴', warning: '🟡', info: '🔵' };
|
||||
const LEVEL_LABEL = { critical: '嚴重', warning: '警告', info: '建議' };
|
||||
const LEVEL_ORDER = ['critical', 'warning', 'info'];
|
||||
|
||||
function findingRow(f) {
|
||||
return `| ${LEVEL_EMOJI[f.level] || ''} ${LEVEL_LABEL[f.level] || f.level} | ${f.role} | ${f.location} | ${f.suggestion} |`;
|
||||
@@ -17,6 +18,12 @@ function buildTable(findings) {
|
||||
}
|
||||
|
||||
const levelText = f => `${LEVEL_EMOJI[f.level] || ''} ${LEVEL_LABEL[f.level] || f.level}`.trim();
|
||||
const bySeverity = (a, b) => {
|
||||
const aLevel = LEVEL_ORDER.includes(a.level) ? LEVEL_ORDER.indexOf(a.level) : LEVEL_ORDER.length;
|
||||
const bLevel = LEVEL_ORDER.includes(b.level) ? LEVEL_ORDER.indexOf(b.level) : LEVEL_ORDER.length;
|
||||
if (aLevel !== bLevel) return aLevel - bLevel;
|
||||
return String(a.location || '').localeCompare(String(b.location || ''));
|
||||
};
|
||||
|
||||
/**
|
||||
* 解析 finding 的 location 取出檔案與行號,供行內 comment 標註使用。
|
||||
@@ -36,6 +43,60 @@ function inlineCommentBody(f) {
|
||||
return `**等級**:${levelText(f)}\n**審查員**:${f.role}\n**建議**:${f.suggestion}`;
|
||||
}
|
||||
|
||||
function problemText(f) {
|
||||
return f.problem || f.title || f.message || f.location || '未提供問題位置';
|
||||
}
|
||||
|
||||
function reviewCommentBody(f) {
|
||||
return [
|
||||
`**嚴重等級**:${levelText(f)}`,
|
||||
`**審查員**:${f.role}`,
|
||||
`**問題**:${problemText(f)}`,
|
||||
`**建議**:${f.suggestion}`,
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
function countBy(findings, predicate) {
|
||||
return findings.filter(predicate).length;
|
||||
}
|
||||
|
||||
function buildReviewSummary(findings, commentable) {
|
||||
const unplacedCount = findings.length - commentable.length;
|
||||
return [
|
||||
'## AI Code Review 統計',
|
||||
'',
|
||||
`總問題:${findings.length} 筆`,
|
||||
`可標註檔案與行數:${commentable.length} 筆`,
|
||||
`無法標註檔案與行數:${unplacedCount} 筆`,
|
||||
`嚴重:${countBy(findings, f => f.level === 'critical')} 筆`,
|
||||
`警告:${countBy(findings, f => f.level === 'warning')} 筆`,
|
||||
`建議:${countBy(findings, f => f.level === 'info')} 筆`,
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
function toReviewComment(f) {
|
||||
const loc = parseLocation(f.location);
|
||||
if (!loc) return null;
|
||||
return {
|
||||
path: loc.file,
|
||||
body: reviewCommentBody(f),
|
||||
new_position: loc.line,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 發布單一 Gitea review:本文統計可找出檔案與行數的問題,
|
||||
* comments 只包含可定位到檔案與行數的 findings,並依嚴重等級排序。
|
||||
*/
|
||||
export async function postFindingsReview(findings, deps = {}) {
|
||||
const { postReview = postPullReview } = deps;
|
||||
const sorted = [...findings].sort(bySeverity);
|
||||
const comments = sorted.map(toReviewComment).filter(Boolean);
|
||||
const body = buildReviewSummary(sorted, comments);
|
||||
await postReview({ body, comments });
|
||||
ok(`review 發布: total=${sorted.length} commentable=${comments.length}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 寫入 findings.json。
|
||||
* 預設寫到 workspace;若提供 mirrorDir,則同步寫入另一份供 repo commit 使用。
|
||||
|
||||
@@ -136,3 +136,20 @@ export async function postPullReviewComment({ path: filePath, line, body }) {
|
||||
);
|
||||
return resp.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 建立一個 PR review,本文放統計摘要,comments 放多筆行內 review comments。
|
||||
*/
|
||||
export async function postPullReview({ body, comments = [] }) {
|
||||
const resp = await axios.post(
|
||||
api(`/repos/${GITEA_REPOSITORY}/pulls/${PR_NUMBER}/reviews`),
|
||||
{
|
||||
commit_id: PR_HEAD_SHA || undefined,
|
||||
event: 'COMMENT',
|
||||
body,
|
||||
comments,
|
||||
},
|
||||
{ headers: headers(GITEA_COMMENT_TOKEN || GITEA_TOKEN), timeout: 30000, httpsAgent },
|
||||
);
|
||||
return resp.data;
|
||||
}
|
||||
|
||||
+4
-6
@@ -3,7 +3,7 @@ import { GITEA_REPOSITORY, PR_NUMBER, PR_HEAD_BRANCH, PR_BASE_BRANCH, getLLMConf
|
||||
import { loadRoles, getRoleIntro } from './roles.js';
|
||||
import { getPRDiff, postComment, 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';
|
||||
@@ -107,16 +107,14 @@ async function main() {
|
||||
const filtered = await filterFalsePositivesWithAI(ruleFiltered, exclusions);
|
||||
ok(`Step4 完成: findings total=${filtered.length}`);
|
||||
|
||||
step('Step5', 'Findings 寫入與 Comment 發布');
|
||||
step('Step5', 'Findings 寫入與 Review 發布');
|
||||
const reviewDir = repoDir || WORKSPACE;
|
||||
saveFindings(WORKSPACE, filtered, reviewDir);
|
||||
try {
|
||||
await postOldFindingsComment(filtered);
|
||||
await postNewNonCriticalComment(filtered);
|
||||
await postNewCriticalComments(filtered);
|
||||
await postFindingsReview(filtered);
|
||||
ok('Step5 完成');
|
||||
} catch (e) {
|
||||
warn(`comment 發布失敗(繼續執行): ${e.message}`);
|
||||
warn(`review 發布失敗(繼續執行): ${e.message}`);
|
||||
}
|
||||
|
||||
step('Step6', 'JSON 格式驗證');
|
||||
|
||||
Reference in New Issue
Block a user