發布 AI review Review 統計與 Step9 合併阻擋調整 #30

Merged
admin merged 20 commits from develop into master 2026-06-22 10:41:37 +00:00
3 changed files with 83 additions and 7 deletions
Showing only changes of commit 757d9ad3c2 - Show all commits
+62 -1
View File
@@ -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 使用。
+17
View File
@@ -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
View File
@@ -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 格式驗證');